瀏覽代碼

Merge branch 'feat/db' into enhance/plugin-web

charlie 1 年之前
父節點
當前提交
898e1ff11f
共有 94 個文件被更改,包括 2397 次插入1695 次删除
  1. 1 0
      .gitignore
  2. 2 0
      deps/db/.carve/ignore
  3. 54 29
      deps/db/src/logseq/db.cljs
  4. 46 5
      deps/db/src/logseq/db/frontend/class.cljs
  5. 2 2
      deps/db/src/logseq/db/frontend/entity_plus.cljc
  6. 52 17
      deps/db/src/logseq/db/frontend/entity_util.cljs
  7. 97 65
      deps/db/src/logseq/db/frontend/malli_schema.cljs
  8. 4 4
      deps/db/src/logseq/db/frontend/order.cljs
  9. 12 12
      deps/db/src/logseq/db/frontend/property.cljs
  10. 1 2
      deps/db/src/logseq/db/frontend/property/build.cljs
  11. 13 5
      deps/db/src/logseq/db/frontend/property/type.cljs
  12. 17 7
      deps/db/src/logseq/db/frontend/rules.cljc
  13. 3 2
      deps/db/src/logseq/db/frontend/schema.cljs
  14. 3 29
      deps/db/src/logseq/db/frontend/validate.cljs
  15. 4 4
      deps/db/src/logseq/db/sqlite/build.cljs
  16. 3 8
      deps/db/src/logseq/db/sqlite/common_db.cljs
  17. 17 9
      deps/db/src/logseq/db/sqlite/create_graph.cljs
  18. 4 4
      deps/db/src/logseq/db/sqlite/util.cljs
  19. 31 6
      deps/db/src/logseq/db/test/helper.cljs
  20. 1 0
      deps/db/test/logseq/db/frontend/inputs_test.cljs
  21. 16 15
      deps/db/test/logseq/db/frontend/rules_test.cljs
  22. 4 4
      deps/db/test/logseq/db/sqlite/build_test.cljs
  23. 18 5
      deps/db/test/logseq/db/sqlite/create_graph_test.cljs
  24. 23 4
      deps/db/test/logseq/db_test.cljs
  25. 10 5
      deps/graph-parser/src/logseq/graph_parser/block.cljs
  26. 105 54
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  27. 1 2
      deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs
  28. 122 107
      deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
  29. 2 3
      deps/graph-parser/test/logseq/graph_parser/extract_test.cljs
  30. 2 1
      deps/outliner/src/logseq/outliner/core.cljs
  31. 12 3
      deps/outliner/src/logseq/outliner/property.cljs
  32. 97 67
      deps/outliner/src/logseq/outliner/validate.cljs
  33. 11 8
      deps/outliner/test/logseq/outliner/pipeline_test.cljs
  34. 3 2
      deps/outliner/test/logseq/outliner/property_test.cljs
  35. 77 33
      deps/outliner/test/logseq/outliner/validate_test.cljs
  36. 450 437
      deps/shui/src/logseq/shui/demo.cljs
  37. 6 0
      deps/shui/src/logseq/shui/ui.cljs
  38. 53 0
      packages/ui/@/components/ui/tabs.tsx
  39. 1 0
      packages/ui/package.json
  40. 3 1
      packages/ui/src/ui.ts
  41. 72 6
      packages/ui/yarn.lock
  42. 3 3
      resources/package.json
  43. 5 1
      scripts/src/logseq/tasks/db_graph/create_graph_with_schema_org.cljs
  44. 2 1
      scripts/src/logseq/tasks/dev/db_and_file_graphs.clj
  45. 8 7
      src/main/frontend/components/all_pages.cljs
  46. 35 26
      src/main/frontend/components/block.cljs
  47. 8 4
      src/main/frontend/components/block.css
  48. 3 3
      src/main/frontend/components/cmdk/core.cljs
  49. 4 4
      src/main/frontend/components/container.cljs
  50. 1 1
      src/main/frontend/components/container.css
  51. 15 14
      src/main/frontend/components/db_based/page.cljs
  52. 10 4
      src/main/frontend/components/editor.cljs
  53. 1 1
      src/main/frontend/components/file_sync.cljs
  54. 3 1
      src/main/frontend/components/icon.cljs
  55. 64 75
      src/main/frontend/components/objects.cljs
  56. 104 56
      src/main/frontend/components/page.cljs
  57. 2 4
      src/main/frontend/components/page.css
  58. 95 45
      src/main/frontend/components/property.cljs
  59. 1 3
      src/main/frontend/components/property.css
  60. 1 1
      src/main/frontend/components/property/config.cljs
  61. 22 11
      src/main/frontend/components/property/value.cljs
  62. 1 1
      src/main/frontend/components/query/builder.cljs
  63. 54 54
      src/main/frontend/components/repo.cljs
  64. 2 2
      src/main/frontend/components/right_sidebar.cljs
  65. 0 4
      src/main/frontend/components/right_sidebar.css
  66. 5 3
      src/main/frontend/components/select.cljs
  67. 0 30
      src/main/frontend/components/title.cljs
  68. 74 79
      src/main/frontend/components/views.cljs
  69. 4 4
      src/main/frontend/context/i18n.cljs
  70. 1 1
      src/main/frontend/db/async.cljs
  71. 92 45
      src/main/frontend/db/model.cljs
  72. 1 1
      src/main/frontend/db/query_dsl.cljs
  73. 28 1
      src/main/frontend/handler/block.cljs
  74. 15 18
      src/main/frontend/handler/db_based/page.cljs
  75. 13 14
      src/main/frontend/handler/graph.cljs
  76. 1 1
      src/main/frontend/handler/journal.cljs
  77. 0 1
      src/main/frontend/handler/page.cljs
  78. 1 1
      src/main/frontend/handler/whiteboard.cljs
  79. 2 1
      src/main/frontend/modules/outliner/ui.cljc
  80. 52 42
      src/main/frontend/ui.cljs
  81. 5 1
      src/main/frontend/ui.css
  82. 64 31
      src/main/frontend/worker/db/migrate.cljs
  83. 1 1
      src/main/frontend/worker/db_worker.cljs
  84. 7 14
      src/main/frontend/worker/export.cljs
  85. 2 2
      src/main/frontend/worker/handler/page.cljs
  86. 51 36
      src/main/frontend/worker/handler/page/db_based/page.cljs
  87. 1 1
      src/main/frontend/worker/rtc/db_listener.cljs
  88. 0 1
      src/main/frontend/worker/rtc/remote_update.cljs
  89. 0 7
      src/main/frontend/worker/search.cljs
  90. 17 16
      src/test/frontend/db/db_based_model_test.cljs
  91. 23 11
      src/test/frontend/worker/handler/page/db_based/page_test.cljs
  92. 32 33
      src/test/frontend/worker/rtc/db_listener_test.cljs
  93. 1 1
      src/test/frontend/worker/rtc/remote_update_test.cljs
  94. 5 5
      static/yarn.lock

+ 1 - 0
.gitignore

@@ -41,6 +41,7 @@ resources/electron.js
 .clj-kondo/metosin/malli
 .clj-kondo/rewrite-clj
 .clj-kondo/taoensso
+.clj-kondo/funcool
 /libs/dist/
 charlie/
 .vscode

+ 2 - 0
deps/db/.carve/ignore

@@ -15,6 +15,8 @@ logseq.db.frontend.inputs/resolve-input
 ;; API
 logseq.db.frontend.class/build-new-class
 ;; API
+logseq.db.frontend.class/page-children-classes
+;; API
 logseq.db.frontend.db-ident/ensure-unique-db-ident
 ;; API
 logseq.db.sqlite.build/create-blocks

+ 54 - 29
deps/db/src/logseq/db.cljs

@@ -17,7 +17,8 @@
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.db.frontend.property :as db-property]
             [logseq.common.util.namespace :as ns-util]
-            [logseq.common.util.page-ref :as page-ref])
+            [logseq.common.util.page-ref :as page-ref]
+            [clojure.walk :as walk])
   (:refer-clojure :exclude [object?]))
 
 (defonce *transact-fn (atom nil))
@@ -40,11 +41,25 @@
              m))
          tx-data)))
 
+(defn assert-no-entities
+  [tx-data]
+  (walk/prewalk
+   (fn [f]
+     (if (de/entity? f)
+       (throw (ex-info "ldb/transact! doesn't support Entity"
+                       {:entity f
+                        :tx-data tx-data}))
+       f))
+   tx-data))
+
 (defn transact!
   "`repo-or-conn`: repo for UI thread and conn for worker/node"
   ([repo-or-conn tx-data]
    (transact! repo-or-conn tx-data nil))
   ([repo-or-conn tx-data tx-meta]
+   (when (and (exists? js/goog)
+              (aget js/goog "DEBUG"))
+     (assert-no-entities tx-data))
    (let [tx-data (map (fn [m]
                         (if (map? m)
                           (dissoc m :block/children :block/meta :block/top? :block/bottom? :block/anchor
@@ -85,6 +100,10 @@
 (def object? entity-util/object?)
 (def asset? entity-util/asset?)
 (def public-built-in-property? db-property/public-built-in-property?)
+(def get-entity-types entity-util/get-entity-types)
+(def internal-tags db-class/internal-tags)
+(def private-tags db-class/private-tags)
+(def hidden-tags db-class/hidden-tags)
 
 (defn sort-by-order
   [blocks]
@@ -176,33 +195,39 @@
 (def db-based-graph? entity-util/db-based-graph?)
 
 (defn page-exists?
-  "Whether a page exists with the `type`."
-  [db page-name type']
+  "Returns truthy value if page exists.
+   For db graphs, returns all page db ids that given title and one of the given `tags`.
+   For file graphs, returns page entity if it exists"
+  [db page-name tags]
   (when page-name
     (if (db-based-graph? db)
-      ;; Classes and properties are case sensitive
-      (if (#{"class" "property"} type')
-        (seq
-         (d/q
-          '[:find [?p ...]
-            :in $ ?name ?type
-            :where
-            [?p :block/title ?name]
-            [?p :block/type ?type]]
-          db
-          page-name
-          type'))
-        ;; TODO: Decouple db graphs from file specific :block/name
-        (seq
-         (d/q
-          '[:find [?p ...]
-            :in $ ?name ?type
-            :where
-            [?p :block/name ?name]
-            [?p :block/type ?type]]
-          db
-          (common-util/page-name-sanity-lc page-name)
-          type')))
+      (let [tags' (if (coll? tags) (set tags) #{tags})]
+        ;; Classes and properties are case sensitive and can be looked up
+        ;; as such in case-sensitive contexts e.g. no Page
+        (if (and (seq tags') (every? #{:logseq.class/Tag :logseq.class/Property} tags'))
+          (seq
+           (d/q
+            '[:find [?p ...]
+              :in $ ?name [?tag-ident ...]
+              :where
+              [?p :block/title ?name]
+              [?p :block/tags ?tag]
+              [?tag :db/ident ?tag-ident]]
+            db
+            page-name
+            tags'))
+          ;; TODO: Decouple db graphs from file specific :block/name
+          (seq
+           (d/q
+            '[:find [?p ...]
+              :in $ ?name [?tag-ident ...]
+              :where
+              [?p :block/name ?name]
+              [?p :block/tags ?tag]
+              [?tag :db/ident ?tag-ident]]
+            db
+            (common-util/page-name-sanity-lc page-name)
+            tags'))))
       (d/entity db [:block/name (common-util/page-name-sanity-lc page-name)]))))
 
 (defn get-page
@@ -448,7 +473,7 @@
    (d/datoms db :avet :block/name)
    (keep (fn [d]
            (let [e (d/entity db (:e d))]
-             (when-not (hidden? e)
+             (when-not (or (hidden? e) (internal-tags (:db/ident e)))
                e))))))
 
 (defn built-in?
@@ -535,7 +560,7 @@
 
 (defn get-all-properties
   [db]
-  (->> (d/datoms db :avet :block/type "property")
+  (->> (d/datoms db :avet :block/tags :logseq.class/Property)
        (map (fn [d]
               (d/entity db (:e d))))))
 
@@ -554,7 +579,7 @@
 
 (defn get-title-with-parents
   [entity]
-  (if (contains? #{"page" "class"} (:block/type entity))
+  (if (or (entity-util/class? entity) (entity-util/internal-page? entity))
     (let [parents' (->> (get-page-parents entity)
                         (remove (fn [e] (= :logseq.class/Root (:db/ident e))))
                         vec)]

+ 46 - 5
deps/db/src/logseq/db/frontend/class.cljs

@@ -2,20 +2,35 @@
   "Class related fns for DB graphs and frontend/datascript usage"
   (:require [logseq.db.sqlite.util :as sqlite-util]
             [logseq.db.frontend.db-ident :as db-ident]
+            [clojure.set :as set]
             [flatland.ordered.map :refer [ordered-map]]))
 
+;; Main class vars
+;; ===============
+
 (def ^:large-vars/data-var built-in-classes
   "Map of built-in classes for db graphs with their :db/ident as keys"
   (ordered-map
    :logseq.class/Root {:title "Root Tag"}
 
-   :logseq.class/Task
-   {:title "Task"
-    :schema {:properties [:logseq.task/status :logseq.task/priority :logseq.task/deadline]}}
+   :logseq.class/Tag {:title "Tag"}
+
+   :logseq.class/Property {:title "Property"}
+
+   :logseq.class/Page {:title "Page"}
 
    :logseq.class/Journal
    {:title "Journal"
-    :properties {:logseq.property.journal/title-format "MMM do, yyyy"}}
+    :properties {:logseq.property/parent :logseq.class/Page
+                 :logseq.property.journal/title-format "MMM do, yyyy"}}
+
+   :logseq.class/Whiteboard
+   {:title "Whiteboard"
+    :properties {:logseq.property/parent :logseq.class/Page}}
+
+   :logseq.class/Task
+   {:title "Task"
+    :schema {:properties [:logseq.task/status :logseq.task/priority :logseq.task/deadline]}}
 
    :logseq.class/Query
    {:title "Query"
@@ -66,6 +81,31 @@
 ;; TODO: Add more classes such as :book, :paper, :movie, :music, :project)
    ))
 
+(def page-children-classes
+  "Children of :logseq.class/Page"
+  (set
+   (keep (fn [[class-ident m]]
+           (when (= (get-in m [:properties :logseq.property/parent]) :logseq.class/Page) class-ident))
+         built-in-classes)))
+
+(def internal-tags
+  "Built-in classes that are hidden on a node and all pages view"
+  #{:logseq.class/Page :logseq.class/Property :logseq.class/Tag :logseq.class/Root
+    :logseq.class/Asset})
+
+(def private-tags
+  "Built-in classes that are private and should not be used by a user directly.
+  These used to be in :block/type"
+  (set/union internal-tags
+             #{:logseq.class/Journal :logseq.class/Whiteboard}))
+
+(def hidden-tags
+  "Built-in classes that are hidden in a few contexts like property values"
+  #{:logseq.class/Page :logseq.class/Root :logseq.class/Asset})
+
+;; Helper fns
+;; ==========
+
 (defn create-user-class-ident-from-name
   "Creates a class :db/ident for a default user namespace.
    NOTE: Only use this when creating a db-ident for a new class."
@@ -78,5 +118,6 @@
   [db page-m]
   {:pre [(string? (:block/title page-m))]}
   (let [db-ident (create-user-class-ident-from-name (:block/title page-m))
-        db-ident' (db-ident/ensure-unique-db-ident db db-ident)]
+        db-ident' (or (:db/ident page-m)
+                      (db-ident/ensure-unique-db-ident db db-ident))]
     (sqlite-util/build-new-class (assoc page-m :db/ident db-ident'))))

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

@@ -27,7 +27,7 @@
   [^Entity e k default-value]
   (let [db (.-db e)
         db-based? (db-based-graph? db)]
-    (if (and db-based? (= "journal" (:block/type e)))
+    (if (and db-based? (entity-util/journal? e))
       (get-journal-title db e)
       (let [search? (get (.-kv e) :block.temp/search?)]
         (or
@@ -65,7 +65,7 @@
        (let [db (.-db e)]
          (case k
            :block/raw-title
-           (if (and (db-based-graph? db) (= "journal" (:block/type e)))
+           (if (and (db-based-graph? db) (entity-util/journal? e))
              (get-journal-title db e)
              (lookup-entity e :block/title default-value))
 

+ 52 - 17
deps/db/src/logseq/db/frontend/entity_util.cljs

@@ -1,7 +1,8 @@
 (ns logseq.db.frontend.entity-util
   "Lower level entity util fns used across db namespaces"
   (:require [datascript.core :as d]
-            [clojure.string :as string])
+            [clojure.string :as string]
+            [datascript.impl.entity :as de])
   (:refer-clojure :exclude [object?]))
 
 (defn db-based-graph?
@@ -10,36 +11,60 @@
   (when db
     (= "db" (:kv/value (d/entity db :logseq.kv/db-type)))))
 
-(defn page?
-  [block]
-  (contains? #{"page" "journal" "whiteboard" "class" "property"}
-             (:block/type block)))
+(defn- has-tag?
+  [entity tag-ident]
+  (let [tags (:block/tags entity)]
+    (some (fn [t] (or (= (:db/ident t) tag-ident)
+                      (= t tag-ident)))
+          (if (coll? tags) tags [tags]))))
 
 (defn internal-page?
   [entity]
-  (= (:block/type entity) "page"))
+  (has-tag? entity :logseq.class/Page))
 
 (defn class?
   [entity]
-  (= (:block/type entity) "class"))
+  (or (= (:db/ident entity) :logseq.class/Tag)
+      (has-tag? entity :logseq.class/Tag)))
 
 (defn property?
   [entity]
-  (= (:block/type entity) "property"))
-
-(defn closed-value?
-  [entity]
-  (= (:block/type entity) "closed value"))
+  (has-tag? entity :logseq.class/Property))
 
 (defn whiteboard?
   "Given a page entity or map, check if it is a whiteboard page"
-  [page]
-  (= (:block/type page) "whiteboard"))
+  [entity]
+  (or
+   ;; db based graph
+   (has-tag? entity :logseq.class/Whiteboard)
+   ;; file based graph
+   (= "whiteboard" (:block/type entity))))
+
+(defn closed-value?
+  [entity]
+  (some? (:block/closed-value-property entity)))
 
 (defn journal?
   "Given a page entity or map, check if it is a journal page"
-  [page]
-  (= (:block/type page) "journal"))
+  [entity]
+  (or
+   ;; db based graph
+   (has-tag? entity :logseq.class/Journal)
+   ;; file based graph
+   (= "journal" (:block/type entity))))
+
+(defn page?
+  [entity]
+  (or
+   ;; db based graph
+   (internal-page? entity)
+   (class? entity)
+   (property? entity)
+   (whiteboard? entity)
+   (journal? entity)
+
+   ;; file based graph
+   (contains? #{"page" "journal" "whiteboard"} (:block/type entity))))
 
 (defn asset?
   "Given an entity or map, check if it is an asset block"
@@ -52,9 +77,19 @@
   (when page
     (if (string? page)
       (string/starts-with? page "$$$")
-      (when (map? page)
+      (when (or (map? page) (de/entity? page))
         (false? (get-in page [:block/schema :public?]))))))
 
 (defn object?
   [node]
   (seq (:block/tags node)))
+
+(defn get-entity-types
+  "Get entity types from :block/tags"
+  [entity]
+  (let [ident->type {:logseq.class/Tag :class
+                     :logseq.class/Property :property
+                     :logseq.class/Journal :journal
+                     :logseq.class/Whiteboard :whiteboard
+                     :logseq.class/Page :page}]
+    (set (map #(ident->type (:db/ident %)) (:block/tags entity)))))

+ 97 - 65
deps/db/src/logseq/db/frontend/malli_schema.cljs

@@ -70,6 +70,15 @@
          (= :logseq.property/empty-placeholder (:db/ident (d/entity db property-val))))
     (= :logseq.property/empty-placeholder property-val)))
 
+(defn internal-ident?
+  "Determines if given ident is created by Logseq. All Logseq internal idents
+   must start with 'block' or 'logseq' to keep Logseq internals from leaking
+   across namespaces and to allow for users and 3rd party plugins to choose
+   any other namespace"
+  [ident]
+  (or (contains? db-property/db-attribute-properties ident)
+      (contains? logseq-ident-namespaces (namespace ident))))
+
 (defn validate-property-value
   "Validates the property value in a property tuple. The property value is
   expected to be a coll if the property has a :many cardinality. validate-fn is
@@ -77,7 +86,7 @@
   validate-fn varies by property type"
   [db validate-fn [{:block/keys [schema] :as property} property-val] & {:keys [new-closed-value?]}]
   ;; For debugging
-  ;; (when (not (string/starts-with? (namespace (:db/ident property)) "logseq.")) (prn :validate-val (dissoc property :property/closed-values) property-val))
+  ;; (when (not (internal-ident? (:db/ident property))) (prn :validate-val (dissoc property :property/closed-values) property-val))
   (let [validate-fn' (if (db-property-type/property-types-with-db (:type schema))
                        (fn [value]
                          (validate-fn db value {:new-closed-value? new-closed-value?}))
@@ -104,30 +113,47 @@
    (set (get-in db-class/built-in-classes [:logseq.class/Asset :schema :required-properties]))
    #{:logseq.property/created-from-property}))
 
+(defn- property-entity->map
+  "Provide the minimal number of property attributes to validate the property
+  and to reduce noise in error messages. The resulting map should be the same as
+  what the frontend property since they both call validate-property-value"
+  [property]
+  ;; use explicit call to be nbb compatible
+  (let [closed-values (entity-plus/lookup-kv-then-entity property :property/closed-values)]
+    (cond-> (assoc (select-keys property [:db/ident :db/valueType :db/cardinality])
+                   :block/schema
+                   (select-keys (:block/schema property) [:type]))
+      (seq closed-values)
+      (assoc :property/closed-values closed-values))))
+
 (defn update-properties-in-ents
   "Prepares properties in entities to be validated by DB schema"
   [db ents]
-  (mapv
-   (fn [ent]
-     (reduce (fn [m [k v]]
-               (if-let [property (and (db-property/property? k)
-                                      ;; This allows schemas like property-value-block to require properties in
-                                      ;; their schema that they depend on
-                                      (not (contains? required-properties k))
-                                      (d/entity db k))]
-                 (update m :block/properties (fnil conj [])
-                         ;; use explicit call to be nbb compatible
-                         [(let [closed-values (entity-plus/lookup-kv-then-entity property :property/closed-values)]
-                            (cond-> (assoc (select-keys property [:db/ident :db/valueType :db/cardinality])
-                                           :block/schema
-                                           (select-keys (:block/schema property) [:type]))
-                              (seq closed-values)
-                              (assoc :property/closed-values closed-values)))
-                          v])
-                 (assoc m k v)))
-             {}
-             ent))
-   ents))
+  ;; required-properties allows schemas like property-value-block to require
+  ;; properties in their schema that they depend on
+  (let [exceptions-to-block-properties (conj required-properties :block/tags)
+        page-class-id (:db/id (d/entity db :logseq.class/Page))
+        private-tag-ids (set (map #(:db/id (d/entity db %)) db-class/private-tags))]
+    (mapv
+     (fn [ent]
+       (reduce (fn [m [k v]]
+                 (if-let [property (and (db-property/property? k)
+                                        (not (contains? exceptions-to-block-properties k))
+                                        (d/entity db k))]
+                   (update m :block/properties (fnil conj [])
+                           [(property-entity->map property) v])
+                   (if (= :block/tags k)
+                     ;; Provides additional options map to validation for data about current entity being tagged
+                     (let [property (d/entity db :block/tags)]
+                       (assoc m k [(property-entity->map property)
+                                   v
+                                   (merge (select-keys ent [:logseq.property/built-in?])
+                                          {:page-class-id page-class-id
+                                           :private-tag-ids private-tag-ids})]))
+                     (assoc m k v))))
+               {}
+               ent))
+     ents)))
 
 (defn datoms->entity-maps
   "Returns entity maps for given :eavt datoms indexed by db/id. Optional keys:
@@ -170,15 +196,6 @@
   (mapv (fn [[db-id m]] (with-meta m {:db/id db-id}))
         (datoms->entity-maps datoms)))
 
-(defn internal-ident?
-  "Determines if given ident is created by Logseq. All Logseq internal idents
-   must start with 'block' or 'logseq' to keep Logseq internals from leaking
-   across namespaces and to allow for users and 3rd party plugins to choose
-   any other namespace"
-  [ident]
-  (or (contains? db-property/db-attribute-properties ident)
-      (contains? logseq-ident-namespaces (namespace ident))))
-
 (assert (every? #(re-find #"^(block|logseq\.)" (namespace %)) db-property/db-attribute-properties)
         "All db-attribute idents start with an internal namespace")
 (assert (every? #(re-find #"^logseq\." %) logseq-ident-namespaces)
@@ -188,14 +205,12 @@
 ;; ==================
 ;; These schemas should be data vars to remain as simple and reusable as possible
 
-
 (def ^:dynamic *db-for-validate-fns*
   "Used by validate-fns which need db as input"
   nil)
 
 (def property-tuple
-  "A tuple of a property map and a property value. This schema
-   has 1 metadata hook which is used to inject a datascript db later"
+  "A tuple of a property map and a property value"
   (into
    [:multi {:dispatch #(-> % first :block/schema :type)}]
    (map (fn [[prop-type value-schema]]
@@ -211,6 +226,24 @@
   property with its property value that is valid for its type"
   [:sequential property-tuple])
 
+(def block-tags
+  [:and
+   ;; FIXME: Display error message instead of 'unknown error'
+   property-tuple
+   ;; Important to keep data integrity of built-in entities. Ensure UI doesn't accidentally modify them
+   [:fn {:error/message "should only have one tag for a built-in entity"}
+    (fn [[_k v opts]]
+      (if (:logseq.property/built-in? opts)
+        (= 1 (count v))
+        true))]
+   ;; Ensure use of :logseq.class/Page is consistent and simple. Doing so reduces complexity elsewhere
+   ;; and allows for Page to exist as its own public concept later
+   #_[:fn {:error/message "should not have other built-in private tags when tagged with #Page"}
+    (fn [[_k v {:keys [page-class-id private-tag-ids]}]]
+      (if (contains? v page-class-id)
+        (empty? (set/intersection (disj v page-class-id) private-tag-ids))
+        true))]])
+
 (def page-or-block-attrs
   "Common attributes for page and normal blocks"
   [[:block/uuid :uuid]
@@ -219,8 +252,8 @@
    [:block/format [:enum :markdown]]
    ;; Injected by update-properties-in-ents
    [:block/properties {:optional true} block-properties]
+   [:block/tags {:optional true} block-tags]
    [:block/refs {:optional true} [:set :int]]
-   [:block/tags {:optional true} [:set :int]]
    [:block/tx-id {:optional true} :int]
    [:block/collapsed? {:optional true} :boolean]])
 
@@ -228,12 +261,7 @@
   "Common attributes for pages"
   [[:block/name :string]
    [:block/title :string]
-   [:block/type [:enum "page" "class" "property" "whiteboard" "journal"]]
-   [:block/alias {:optional true} [:set :int]]
-    ;; TODO: Should this be here or in common?
-   [:block/path-refs {:optional true} [:set :int]]
-   ;; file-based
-   [:block/namespace {:optional true} :int]])
+   [:block/path-refs {:optional true} [:set :int]]])
 
 (def property-attrs
   "Common attributes for properties"
@@ -302,7 +330,8 @@
   (vec
    (concat
     [:map
-     [:db/ident user-property-ident]
+     ;; class-ident allows for a class to be used as a property
+     [:db/ident [:or user-property-ident class-ident]]
      [:block/schema user-property-schema]]
     property-attrs
     page-attrs
@@ -364,8 +393,7 @@
   (vec
    (concat
     [:map]
-    [[:block/type [:= "closed value"]]
-     ;; for built-in properties
+    [;; for built-in properties
      [:db/ident {:optional true} logseq-property-ident]
      [:block/title {:optional true} :string]
      [:property.value/content {:optional true} [:or :string :double]]
@@ -436,27 +464,30 @@
   (into
    [:multi {:dispatch (fn [d]
                         ;; order matters as some block types are a subset of others e.g. :whiteboard
-                        (cond
-                          (entity-util/property? d)
-                          :property
-                          (entity-util/class? d)
-                          :class
-                          (entity-util/hidden? d)
-                          :hidden
-                          (entity-util/whiteboard? d)
-                          :normal-page
-                          (entity-util/page? d)
-                          :normal-page
-                          (entity-util/asset? d)
-                          :asset-block
-                          (:file/path d)
-                          :file-block
-                          (:block/uuid d)
-                          :block
-                          (= (:db/ident d) :logseq.property/empty-placeholder)
-                          :property-value-placeholder
-                          (:db/ident d)
-                          :db-ident-key-value))}]
+                        (let [db *db-for-validate-fns*
+                              d (if (:block/uuid d) (d/entity db [:block/uuid (:block/uuid d)]) d)
+                              dispatch-key (cond
+                                             (entity-util/property? d)
+                                             :property
+                                             (entity-util/class? d)
+                                             :class
+                                             (entity-util/hidden? (:block/title d))
+                                             :hidden
+                                             (entity-util/whiteboard? d)
+                                             :normal-page
+                                             (entity-util/page? d)
+                                             :normal-page
+                                             (entity-util/asset? d)
+                                             :asset-block
+                                             (:file/path d)
+                                             :file-block
+                                             (:block/uuid d)
+                                             :block
+                                             (= (:db/ident d) :logseq.property/empty-placeholder)
+                                             :property-value-placeholder
+                                             (:db/ident d)
+                                             :db-ident-key-value)]
+                          dispatch-key))}]
    {:property property-page
     :class class-page
     :hidden hidden-page
@@ -479,6 +510,7 @@
 (let [malli-many-ref-attrs (->> (concat property-attrs page-attrs block-attrs page-or-block-attrs (rest closed-value-block*))
                                 (filter #(= (last %) [:set :int]))
                                 (map first)
+                                (into db-property/public-db-attribute-properties)
                                 set)]
   (when-let [undeclared-ref-attrs (seq (remove malli-many-ref-attrs db-schema/card-many-ref-type-attributes))]
     (throw (ex-info (str "The malli DB schema is missing the following cardinality-many ref attributes from datascript's schema: "

+ 4 - 4
deps/db/src/logseq/db/frontend/order.cljs

@@ -10,8 +10,8 @@
    (reset-max-key! *max-key key))
   ([max-key-atom key]
    (when (and key (or (nil? @max-key-atom)
-                     (> (compare key @max-key-atom) 0)))
-    (reset! max-key-atom key))))
+                      (> (compare key @max-key-atom) 0)))
+     (reset! max-key-atom key))))
 
 (defn gen-key
   ([]
@@ -50,7 +50,7 @@
                 (when (and (< (compare (:block/order e) (:block/order value)) 0)
                            (not= (:db/id e) (:db/id value)))
                   (:block/order e))) values))
-      (let [properties (->> (d/datoms db :avet :block/type "property")
+      (let [properties (->> (d/datoms db :avet :block/tags :logseq.class/Property)
                             (map (fn [d] (d/entity db (:e d))))
                             (sort-by :block/order)
                             reverse)]
@@ -68,7 +68,7 @@
                 (when (and (> (compare (:block/order e) (:block/order value)) 0)
                            (not= (:db/id e) (:db/id value)))
                   (:block/order e))) values))
-      (let [properties (->> (d/datoms db :avet :block/type "property")
+      (let [properties (->> (d/datoms db :avet :block/tags :logseq.class/Property)
                             (map (fn [d] (d/entity db (:e d))))
                             (sort-by :block/order))]
         (some (fn [property]

+ 12 - 12
deps/db/src/logseq/db/frontend/property.cljs

@@ -48,12 +48,6 @@
                                         :schema {:type :any
                                                  :public? false
                                                  :hide? true}}
-   :block/type           {:title "Node Type"
-                          :attribute :block/type
-                          :schema {:type :string
-                                   :public? false
-                                   :hide? true}
-                          :queryable? true}
    :block/schema         {:title "Node schema"
                           :attribute :block/schema
                           :schema {:type :map
@@ -451,13 +445,18 @@
 
 (def db-attribute-properties
   "Internal properties that are also db schema attributes"
-  #{:block/alias :block/tags :block/type :block/schema :block/parent
+  #{:block/alias :block/tags :block/schema :block/parent
     :block/order :block/collapsed? :block/page
     :block/refs :block/path-refs :block/link
     :block/title :block/closed-value-property
     :block/created-at :block/updated-at
     :logseq.property.attribute/kv-value :logseq.property.attribute/property-schema-classes :logseq.property.attribute/property-value-content})
 
+(assert (= db-attribute-properties
+           (set (keep (fn [[k {:keys [attribute]}]] (when attribute k))
+                      built-in-properties)))
+        "All db attribute properties are configured in built-in-properties")
+
 (def private-db-attribute-properties
   "db-attribute properties that are not visible to user"
   (->> db-attribute-properties
@@ -472,11 +471,6 @@
   "Property values that shouldn't be updated"
   #{:logseq.property/built-in?})
 
-(assert (= db-attribute-properties
-           (set (keep (fn [[k {:keys [attribute]}]] (when attribute k))
-                      built-in-properties)))
-        "All db attribute properties are configured in built-in-properties")
-
 (def logseq-property-namespaces
   #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.property.fsrs" "logseq.task"
     "logseq.property.linked-references" "logseq.property.asset" "logseq.property.table" "logseq.property.node"
@@ -496,6 +490,11 @@
   [s]
   (string/includes? s ".property"))
 
+(defn user-class-namespace?
+  "Determines if namespace string is a user class"
+  [s]
+  (string/includes? s ".class"))
+
 (defn property?
   "Determines if ident kw is a property visible to user"
   [k]
@@ -503,6 +502,7 @@
     (and k-name
          (or (contains? logseq-property-namespaces k-name)
              (user-property-namespace? k-name)
+             (user-class-namespace? k-name)
              ;; disallow private db-attribute-properties as they cause unwanted refs
              ;; and appear noisily in debugging contexts
              (and (keyword? k) (contains? public-db-attribute-properties k))))))

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

@@ -8,8 +8,7 @@
 (defn- closed-value-new-block
   [block-id block-type value property]
   (let [property-id (:db/ident property)]
-    (merge {:block/type "closed value"
-            :block/format :markdown
+    (merge {:block/format :markdown
             :block/uuid block-id
             :block/page property-id
             :block/closed-value-property property-id

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

@@ -138,7 +138,7 @@
   [db val]
   (when-let [ent (d/entity db val)]
     (and (some? (:block/title ent))
-         (= (:block/type ent) "journal"))))
+         (entity-util/journal? ent))))
 
 (def built-in-validation-schemas
   "Map of types to malli validation schemas that validate a property value for that type"
@@ -167,10 +167,18 @@
 
    :string   string?
    :raw-number number?
-   :entity   entity?
-   :class    class-entity?
-   :property property-entity?
-   :page     page-entity?
+   :entity   [:fn
+              {:error/message "should be an Entity"}
+              entity?]
+   :class    [:fn
+              {:error/message "should be a Class"}
+              class-entity?]
+   :property [:fn
+              {:error/message "should be a Property"}
+              property-entity?]
+   :page     [:fn
+              {:error/message "should be a Page"}
+              page-entity?]
    :keyword  keyword?
    :map      map?
    ;; coll elements are ordered as it's saved as a vec

+ 17 - 7
deps/db/src/logseq/db/frontend/rules.cljc

@@ -155,8 +155,18 @@
    (dissoc query-dsl-rules :namespace
            :page-property :has-page-property
            :page-tags :all-page-tags)
+
    (dissoc rules :namespace)
-   {:existing-property-value
+
+   {:between
+    '[(between ?b ?start ?end)
+      [?b :block/page ?p]
+      [?p :block/tags :logseq.class/Journal]
+      [?p :block/journal-day ?d]
+      [(>= ?d ?start)]
+      [(<= ?d ?end)]]
+
+    :existing-property-value
     '[;; non-ref value
       [(existing-property-value ?b ?prop ?val)
        [?prop-e :db/ident ?prop]
@@ -229,7 +239,7 @@
     :has-simple-query-property
     '[(has-simple-query-property ?b ?prop)
       [?prop-e :db/ident ?prop]
-      [?prop-e :block/type "property"]
+      [?prop-e :block/tags :logseq.class/Property]
       (has-property-or-default-value? ?b ?prop)
       [?prop-e :block/schema ?prop-schema]
       [(get ?prop-schema :public? true) ?public]
@@ -239,7 +249,7 @@
     :has-private-simple-query-property
     '[(has-private-simple-query-property ?b ?prop)
       [?prop-e :db/ident ?prop]
-      [?prop-e :block/type "property"]
+      [?prop-e :block/tags :logseq.class/Property]
       (has-property-or-default-value? ?b ?prop)]
 
     ;; Checks if a property exists for any features that are not simple queries
@@ -247,7 +257,7 @@
     '[(has-property ?b ?prop)
       [?b ?prop _]
       [?prop-e :db/ident ?prop]
-      [?prop-e :block/type "property"]
+      [?prop-e :block/tags :logseq.class/Property]
       [?prop-e :block/schema ?prop-schema]
       [(get ?prop-schema :public? true) ?public]
       [(= true ?public)]]
@@ -256,7 +266,7 @@
     :property
     '[(property ?b ?prop ?val)
       [?prop-e :db/ident ?prop]
-      [?prop-e :block/type "property"]
+      [?prop-e :block/tags :logseq.class/Property]
       [?prop-e :block/schema ?prop-schema]
       [(get ?prop-schema :public? true) ?public]
       [(= true ?public)]
@@ -276,7 +286,7 @@
     :simple-query-property
     '[(simple-query-property ?b ?prop ?val)
       [?prop-e :db/ident ?prop]
-      [?prop-e :block/type "property"]
+      [?prop-e :block/tags :logseq.class/Property]
       [?prop-e :block/schema ?prop-schema]
       [(get ?prop-schema :public? true) ?public]
       [(get ?prop-schema :type) ?type]
@@ -287,7 +297,7 @@
     :private-simple-query-property
     '[(private-simple-query-property ?b ?prop ?val)
       [?prop-e :db/ident ?prop]
-      [?prop-e :block/type "property"]
+      [?prop-e :block/tags :logseq.class/Property]
       (property-value ?b ?prop-e ?val)]
 
     :tags

+ 3 - 2
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 51)
+(def version 52)
 
 ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
 (def ^:large-vars/data-var schema
@@ -111,7 +111,8 @@
    (dissoc schema
            :block/namespace :block/properties-text-values :block/pre-block? :recent/pages :block/file
            :block/properties :block/properties-order :block/repeated? :block/deadline :block/scheduled :block/priority
-           :block/marker :block/macros)
+           :block/marker :block/macros
+           :block/type)
    {:block/name {:db/index true}        ; remove db/unique for :block/name
     ;; closed value
     :block/closed-value-property {:db/valueType :db.type/ref

+ 3 - 29
deps/db/src/logseq/db/frontend/validate.cljs

@@ -19,32 +19,11 @@
   [closed-schema?]
   (if closed-schema? closed-db-schema-explainer db-schema-explainer))
 
-(defn validate-ents-before-after!
-  [changed-ids db-before db-after tx-data tx-meta]
-  (let [id->ent-before (into {}
-                             (keep (fn [id] (when-let [ent (d/entity db-before id)] [id ent])))
-                             changed-ids)
-        id->ent-after (keep (fn [id] (when-let [ent (d/entity db-after id)] [id ent])) changed-ids)
-        ent-before+ent-after-coll
-        (reduce
-         (fn [acc [id ent-after]]
-           (if-let [ent-before (id->ent-before id)]
-             (conj acc [ent-before ent-after])
-             acc))
-         [] id->ent-after)]
-    (doseq [[ent-before ent-after] ent-before+ent-after-coll]
-      (let [[type-before type-after] [(:block/type ent-before) (:block/type ent-after)]]
-        (when (and (some? type-before)
-                   (nil? type-after))
-          (js/console.error "Illegal :block/type change, entity id:" (:db/id ent-after))
-          (prn :ent-before ent-before :ent-after ent-after :tx-data tx-data :tx-meta tx-meta))))))
-
 (defn validate-tx-report!
   "Validates the datascript tx-report for entities that have changed. Returns
   boolean indicating if db is valid"
-  [{:keys [db-before db-after tx-data tx-meta]} validate-options]
+  [{:keys [db-after tx-data tx-meta]} validate-options]
   (let [changed-ids (->> tx-data (keep :e) distinct)
-        _ (validate-ents-before-after! changed-ids db-before db-after tx-data tx-meta)
         tx-datoms (mapcat #(d/datoms db-after :eavt %) changed-ids)
         ent-maps* (map (fn [[db-id m]]
                          ;; Add :db/id for debugging
@@ -62,13 +41,8 @@
           (let [explainer (get-schema-explainer (:closed-schema? validate-options))]
             (js/console.error "Invalid datascript entities detected amongst changed entity ids:" changed-ids)
             (doseq [m invalid-ent-maps]
-
               (prn {:entity-map m
-                    :errors (me/humanize (explainer [m]))})
-          ;; FIXME: pprint fails sometime
-          ;; (pprint/pprint {;; :entity-map (map #(into {} %) m)
-          ;;                 :errors (me/humanize (m/explain db-schema [m]))})
-              )
+                    :errors (me/humanize (explainer [m]))}))
             false)
           true)))))
 
@@ -88,7 +62,7 @@
                            (:block/page ent)
                            (update :block/page
                                    (fn [id] (select-keys (d/entity db id)
-                                                         [:block/name :block/type :db/id :block/created-at])))))
+                                                         [:block/name :block/tags :db/id :block/created-at])))))
                :errors errors'
                ;; Group by type to reduce verbosity
                ;; TODO: Move/remove this to another fn if unused

+ 4 - 4
deps/db/src/logseq/db/sqlite/build.cljs

@@ -357,7 +357,7 @@
                       {:db/id (or (:db/id page) (new-db-id))
                        :block/title (or (:block/title page) (string/capitalize (:block/name page)))
                        :block/name (or (:block/name page) (common-util/page-name-sanity-lc (:block/title page)))
-                       :block/type "page"
+                       :block/tags #{:logseq.class/Page}
                        :block/format :markdown}
                       (dissoc page :build/properties :db/id :block/name :block/title :build/tags))
             pvalue-tx-m (->property-value-tx-m new-page (:build/properties page) properties all-idents)]
@@ -376,8 +376,9 @@
                                     page-uuids
                                     all-idents))
               (when-let [tags (:build/tags page)]
-                {:block/tags (mapv #(hash-map :db/ident (get-ident all-idents %))
-                                   tags)})))))
+                {:block/tags (-> (mapv #(hash-map :db/ident (get-ident all-idents %))
+                                       tags)
+                                 (conj :logseq.class/Page))})))))
          ;; blocks tx
          (reduce (fn [acc m]
                    (into acc
@@ -480,7 +481,6 @@
                                                    :block/title page-name
                                                    :block/uuid
                                                    (common-uuid/gen-uuid :journal-page-uuid date-int)
-                                                   :block/type "journal"
                                                    :block/tags :logseq.class/Journal})))))
                            m))]
     ;; Order matters as some steps depend on previous step having prepared blocks or pages in a certain way

+ 3 - 8
deps/db/src/logseq/db/sqlite/common_db.cljs

@@ -186,7 +186,6 @@
             :in $ ?today
             :where
             [?page :block/name ?page-name]
-            ;; [?page :block/type "journal"]
             [?page :block/journal-day ?journal-day]
             [(<= ?journal-day ?today)]]
           db
@@ -213,13 +212,9 @@
 
 (defn get-structured-datoms
   [db]
-  (mapcat (fn [type']
-            (->> (d/datoms db :avet :block/type type')
-                 (mapcat (fn [d]
-                           (d/datoms db :eavt (:e d))))))
-          [;; property and class pages are pulled from `get-all-pages` already
-           ;; "property" "class"
-           "closed value"]))
+  (->> (d/datoms db :avet :block/closed-value-property)
+       (mapcat (fn [d]
+                 (d/datoms db :eavt (:e d))))))
 
 (defn get-favorites
   "Favorites page and its blocks"

+ 17 - 9
deps/db/src/logseq/db/sqlite/create_graph.cljs

@@ -10,7 +10,8 @@
             [logseq.db.frontend.schema :as db-schema]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.common.config :as common-config]
-            [logseq.db.frontend.order :as db-order]))
+            [logseq.db.frontend.order :as db-order]
+            [logseq.db.frontend.entity-util :as entity-util]))
 
 (defn- mark-block-as-built-in [block]
   (assoc block :logseq.property/built-in? true))
@@ -52,14 +53,15 @@
                    ;; Adding built-ins must come after initial properties
                    [(mark-block-as-built-in' built-in-property)]
                    (map mark-block-as-built-in' properties)
-                   (keep #(when (= "closed value" (:block/type %)) (mark-block-as-built-in' %))
+                   (keep #(when (entity-util/closed-value? %)
+                            (mark-block-as-built-in' %))
                          properties))]
     (doseq [m tx]
       (when-let [block-uuid (and (:db/ident m) (:block/uuid m))]
         (assert (string/starts-with? (str block-uuid) "00000002") m)))
 
     {:tx tx
-     :properties (filter #(= (:block/type %) "property") properties)}))
+     :properties (filter entity-util/property? properties)}))
 
 (def built-in-pages-names
   #{"Contents"})
@@ -69,7 +71,6 @@
              (->> (keep :db/ident tx)
                   frequencies
                   (keep (fn [[k v]] (when (> v 1) k)))
-                  (remove #{:logseq.class/Root})
                   seq)]
     (throw (ex-info (str "The following :db/idents are not unique and clobbered each other: "
                          (vec conflicting-idents))
@@ -111,7 +112,7 @@
       {:block/uuid page-id
        :block/name common-config/views-page-name
        :block/title common-config/views-page-name
-       :block/type "page"
+       :block/tags [:logseq.class/Page]
        :block/schema {:public? false}
        :block/format :markdown
        :logseq.property/built-in? true})
@@ -131,7 +132,7 @@
     {:block/uuid (common-uuid/gen-uuid)
      :block/name common-config/favorites-page-name
      :block/title common-config/favorites-page-name
-     :block/type "page"
+     :block/tags [:logseq.class/Page]
      :block/schema {:public? false}
      :block/format :markdown
      :logseq.property/built-in? true})])
@@ -147,8 +148,7 @@
                        (sqlite-util/kv :logseq.kv/graph-initial-schema-version db-schema/version)
                        (sqlite-util/kv :logseq.kv/graph-created-at (common-util/time-ms))
                        ;; Empty property value used by db.type/ref properties
-                       {:db/ident :logseq.property/empty-placeholder}
-                       {:db/ident :logseq.class/Root}]
+                       {:db/ident :logseq.property/empty-placeholder}]
                        import-type
                        (into (sqlite-util/import-tx import-type)))
         initial-files [{:block/uuid (d/squuid)
@@ -172,7 +172,15 @@
         default-pages (->> (map sqlite-util/build-new-page built-in-pages-names)
                            (map mark-block-as-built-in))
         hidden-pages (concat (build-initial-views) (build-favorites-page))
-        tx (vec (concat initial-data properties-tx default-classes
+        ;; These classes bootstrap our tags and properties as they depend on each other e.g.
+        ;; Root <-> Tag, classes-tx depends on logseq.property/parent, properties-tx depends on Property
+        bootstrap-class? (fn [c] (contains? #{:logseq.class/Root :logseq.class/Property :logseq.class/Tag} (:db/ident c)))
+        bootstrap-classes (filter bootstrap-class? default-classes)
+        bootstrap-class-ids (map #(select-keys % [:db/ident :block/uuid]) bootstrap-classes)
+        classes-tx (concat (map #(dissoc % :db/ident) bootstrap-classes)
+                           (remove bootstrap-class? default-classes))
+        ;; Order of tx is critical. bootstrap-class-ids bootstraps properties-tx and classes-tx
+        tx (vec (concat bootstrap-class-ids initial-data properties-tx classes-tx
                         initial-files default-pages hidden-pages))]
     (validate-tx-for-duplicate-idents tx)
     tx))

+ 4 - 4
deps/db/src/logseq/db/sqlite/util.cljs

@@ -91,7 +91,7 @@
      (block-with-timestamps
       (cond->
        {:db/ident db-ident'
-        :block/type "property"
+        :block/tags #{:logseq.class/Property}
         :block/format :markdown
         :block/schema (merge {:type :default} (dissoc prop-schema :classes :cardinality))
         :block/name (common-util/page-name-sanity-lc (name prop-name))
@@ -115,8 +115,8 @@
   {:pre [(qualified-keyword? (:db/ident block))]}
   (block-with-timestamps
    (cond-> (merge block
-                  {:block/type "class"
-                   :block/format :markdown})
+                  {:block/format :markdown
+                   :block/tags (set (conj (:block/tags block) :logseq.class/Tag))})
      (and (not= (:db/ident block) :logseq.class/Root)
           (nil? (:logseq.property/parent block)))
      (assoc :logseq.property/parent :logseq.class/Root))))
@@ -129,7 +129,7 @@
     :block/title page-name
     :block/uuid (d/squuid)
     :block/format :markdown
-    :block/type "page"}))
+    :block/tags #{:logseq.class/Page}}))
 
 (defn kv
   "Creates a key-value pair tx with the key and value respectively stored under

+ 31 - 6
deps/db/src/logseq/db/test/helper.cljs

@@ -5,13 +5,38 @@
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.frontend.schema :as db-schema]))
 
-(defn find-block-by-content [db content]
-  (->> content
-       (d/q '[:find [(pull ?b [*]) ...]
-              :in $ ?content
-              :where [?b :block/title ?content]]
+(defn find-block-by-content
+  "Find first block by exact block string or by fuzzier regex"
+  [db content]
+  (if (instance? js/RegExp content)
+    (->> content
+         (d/q '[:find [(pull ?b [*]) ...]
+                :in $ ?pattern
+                :where
+                [?b :block/title ?content]
+                [?b :block/page]
+                [(re-find ?pattern ?content)]]
+              db)
+         first)
+    (->> content
+         (d/q '[:find [(pull ?b [*]) ...]
+                :in $ ?content
+                :where
+                [?b :block/title ?content]
+                [?b :block/page]]
+              db)
+         first)))
+
+(defn find-page-by-title
+  "Find first page by its title"
+  [db title]
+  (->> title
+       (d/q '[:find [?b ...]
+              :in $ ?title
+              :where [?b :block/title ?title]]
             db)
-       first))
+       first
+       (d/entity db)))
 
 (defn create-conn
   "Create a conn for a DB graph seeded with initial data"

+ 1 - 0
deps/db/test/logseq/db/frontend/inputs_test.cljs

@@ -18,6 +18,7 @@
 
 (deftest resolve-input-for-page-and-block-inputs
   (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
+        _ (d/transact! conn [{:db/ident :logseq.class/Page}])
         _ (sqlite-build/create-blocks
            conn
            [{:page {:block/title "page1"}

+ 16 - 15
deps/db/test/logseq/db/frontend/rules_test.cljs

@@ -31,10 +31,10 @@
               {:properties {:foo {:block/schema {:type :default}}
                             :foo2 {:block/schema {:type :default}}}
                :pages-and-blocks
-               [{:page {:block/title "Page"
+               [{:page {:block/title "Page1"
                         :build/properties {:foo "bar"}}}]})]
 
-    (is (= ["Page"]
+    (is (= ["Page1"]
            (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (has-property ?b :user.property/foo)]
                               @conn)
                 (map (comp :block/title first))))
@@ -44,9 +44,9 @@
                               @conn)
                 (map (comp :block/title first))))
         "has-property returns no result when block doesn't have property")
-    (is (= [:user.property/foo]
+    (is (= [:user.property/foo :block/tags]
            (q-with-rules '[:find [?p ...]
-                           :where (has-property ?b ?p) [?b :block/title "Page"]]
+                           :where (has-property ?b ?p) [?b :block/title "Page1"]]
                          @conn))
         "has-property can bind to property arg")))
 
@@ -57,12 +57,12 @@
                             :number-many {:block/schema {:type :number :cardinality :many}}
                             :page-many {:block/schema {:type :node :cardinality :many}}}
                :pages-and-blocks
-               [{:page {:block/title "Page"
+               [{:page {:block/title "Page1"
                         :build/properties {:foo "bar" :number-many #{5 10} :page-many #{[:page "Page A"]}}}}
                 {:page {:block/title "Page A"
                         :build/properties {:foo "bar A"}}}]})]
     (testing "cardinality :one property"
-      (is (= ["Page"]
+      (is (= ["Page1"]
              (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/foo "bar")]
                                 @conn)
                   (map (comp :block/title first))))
@@ -74,13 +74,13 @@
           "property returns no result when page doesn't have property value")
       (is (= #{:user.property/foo}
              (->> (q-with-rules '[:find [?p ...]
-                                  :where (property ?b ?p "bar") [?b :block/title "Page"]]
+                                  :where (property ?b ?p "bar") [?b :block/title "Page1"]]
                                 @conn)
                   set))
           "property can bind to property arg with bound property value"))
 
     (testing "cardinality :many property"
-      (is (= ["Page"]
+      (is (= ["Page1"]
              (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/number-many 5)]
                                 @conn)
                   (map (comp :block/title first))))
@@ -92,14 +92,14 @@
           "property returns no result when page doesn't have property value")
       (is (= #{:user.property/number-many}
              (->> (q-with-rules '[:find [?p ...]
-                                  :where (property ?b ?p 5) [?b :block/title "Page"]]
+                                  :where (property ?b ?p 5) [?b :block/title "Page1"]]
                                 @conn)
                   set))
           "property can bind to property arg with bound property value"))
 
     ;; NOTE: Querying a ref's name is different than before and requires more than just the rule
     (testing ":ref property"
-      (is (= ["Page"]
+      (is (= ["Page1"]
              (->> (q-with-rules '[:find (pull ?b [:block/title])
                                   :where (property ?b :user.property/page-many "Page A")]
                                 @conn)
@@ -113,22 +113,23 @@
           "property returns no result when page doesn't have property value"))
 
     (testing "bindings with property value"
-      (is (= #{:user.property/foo :user.property/number-many :user.property/page-many}
+      (is (= #{:user.property/foo :user.property/number-many :user.property/page-many :block/tags}
              (->> (q-with-rules '[:find [?p ...]
-                                  :where (property ?b ?p _) [?b :block/title "Page"]]
+                                  :where (property ?b ?p _) [?b :block/title "Page1"]]
                                 @conn)
                   set))
           "property can bind to property arg with unbound property value")
       (is (= #{[:user.property/number-many 10]
                [:user.property/number-many 5]
                [:user.property/foo "bar"]
-               [:user.property/page-many "Page A"]}
+               [:user.property/page-many "Page A"]
+               [:block/tags "Page"]}
              (->> (q-with-rules '[:find ?p ?val
-                                  :where (property ?b ?p ?val) [?b :block/title "Page"]]
+                                  :where (property ?b ?p ?val) [?b :block/title "Page1"]]
                                 @conn)
                   set))
           "property can bind to property and property value args")
-      (is (= #{"Page"}
+      (is (= #{"Page1"}
              (->> (q-with-rules '[:find (pull ?b [:block/title])
                                   :where
                                   [?b :user.property/page-many ?pv]

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

@@ -12,14 +12,14 @@
            [{:page {:block/title "page1"}
              :blocks [{:block/title "Jrue Holiday" :build/tags [:Person]}]}
             {:page {:block/title "Jayson Tatum" :build/tags [:Person]}}])]
-    (is (= {:block/tags [{:block/title "Person", :block/type "class"}]}
-           (first (d/q '[:find [(pull ?b [{:block/tags [:block/title :block/type]}]) ...]
+    (is (= {:block/tags [{:block/title "Person"}]}
+           (first (d/q '[:find [(pull ?b [{:block/tags [:block/title]}]) ...]
                          :where [?b :block/title "Jrue Holiday"]]
                        @conn)))
         "Person class is created and correctly associated to a block")
 
-    (is (= {:block/tags [{:block/title "Person", :block/type "class"}]}
-           (first (d/q '[:find [(pull ?b [{:block/tags [:block/title :block/type]}]) ...]
+    (is (= {:block/tags [{:block/title "Page"} {:block/title "Person"}]}
+           (first (d/q '[:find [(pull ?b [{:block/tags [:block/title]}]) ...]
                          :where [?b :block/title "Jayson Tatum"]]
                        @conn)))
         "Person class is created and correctly associated to a page")))

+ 18 - 5
deps/db/test/logseq/db/sqlite/create_graph_test.cljs

@@ -9,15 +9,16 @@
             [logseq.db.frontend.property :as db-property]
             [logseq.db.sqlite.build :as sqlite-build]
             [logseq.db :as ldb]
-            [logseq.db.test.helper :as db-test]))
+            [logseq.db.test.helper :as db-test]
+            [logseq.db.frontend.class :as db-class]))
 
 (deftest new-graph-db-idents
   (testing "a new graph follows :db/ident conventions for"
     (let [conn (db-test/create-conn)
-          ident-ents (->> (d/q '[:find (pull ?b [:db/ident :block/type])
+          ident-ents (->> (d/q '[:find [?b ...]
                                  :where [?b :db/ident]]
                                @conn)
-                          (map first))
+                          (map (fn [id] (d/entity @conn id))))
           default-idents (map :db/ident ident-ents)]
       (is (> (count default-idents) 45)
           "Approximate number of default idents is correct")
@@ -38,7 +39,7 @@
                                            (map #(keyword (namespace %) (string/replace (name %) #".[^.]+$" "")))
                                            set)]
           (is (= []
-                 (remove #(= "closed value" (:block/type %)) closed-value-ents))
+                 (remove ldb/closed-value? closed-value-ents))
               "All property names that contain a '.' are closed values")
           (is (= #{}
                  (set/difference
@@ -56,7 +57,7 @@
                     (remove #(or (= "logseq.kv" (namespace (:db/ident %)))
                                  (= :logseq.property/empty-placeholder (:db/ident %)))))
         pages (d/q '[:find [(pull ?b [:logseq.property/built-in? :block/title]) ...]
-                     :where [?b :block/type "page"]]
+                     :where [?b :block/tags :logseq.class/Page]]
                    @conn)]
     (is (= [] (remove :logseq.property/built-in? idents))
         "All entities with :db/ident have built-in property (except for kv idents)")
@@ -73,6 +74,18 @@
     (is (every? ldb/property? (:logseq.property.class/properties task))
         "Each task property has correct type")))
 
+(deftest new-graph-initializes-default-classes-correctly
+  (let [conn (db-test/create-conn)]
+    (is (= (count db-class/built-in-classes) (count (d/datoms @conn :avet :block/tags :logseq.class/Tag)))
+        "All built-in classes have a :logseq.class/Tag")
+
+    (is (= (count (dissoc db-class/built-in-classes :logseq.class/Root))
+           (count (->> (d/datoms @conn :avet :block/tags :logseq.class/Tag)
+                       (map #(d/entity @conn (:e %)))
+                       (mapcat :logseq.property/_parent)
+                       set)))
+        "Reverse lookup of :logseq.property/parent correctly fetches number of child classes")))
+
 (deftest new-graph-is-valid
   (let [conn (db-test/create-conn)
         validation (db-validate/validate-db! @conn)]

+ 23 - 4
deps/db/test/logseq/db_test.cljs

@@ -29,16 +29,16 @@
                   (ex-message e)))))))
 
 (def class-parents-data
-  [{:block/type "class"
+  [{:block/tags :logseq.class/Tag
     :block/title "x"
     :block/name "x"
     :block/uuid #uuid "6c353967-f79b-4785-b804-a39b81d72461"}
-   {:block/type "class"
+   {:block/tags :logseq.class/Tag
     :block/title "y"
     :block/name "y"
     :block/uuid #uuid "7008db08-ba0c-4aa9-afc6-7e4783e40a99"
     :logseq.property/parent [:block/uuid #uuid "6c353967-f79b-4785-b804-a39b81d72461"]}
-   {:block/type "class"
+   {:block/tags :logseq.class/Tag
     :block/title "z"
     :block/name "z"
     :block/uuid #uuid "d95f2912-a7af-41b9-8ed5-28861f7fc0be"
@@ -63,4 +63,23 @@
     (is (= "Foo" (:block/title (ldb/get-case-page @conn "Foo"))))
     ;; Case sensitive classes
     (is (= "movie" (:block/title (ldb/get-case-page @conn "movie"))))
-    (is (= "Movie" (:block/title (ldb/get-case-page @conn "Movie"))))))
+    (is (= "Movie" (:block/title (ldb/get-case-page @conn "Movie"))))))
+
+(deftest page-exists
+  (let [conn (db-test/create-conn-with-blocks
+              {:properties
+               {:foo {:block/schema {:type :default}}
+                :Foo {:block/schema {:type :default}}}
+               :classes {:movie {} :Movie {}}})]
+    (is (= ["foo"]
+           (map #(:block/title (d/entity @conn %)) (ldb/page-exists? @conn "foo" #{:logseq.class/Property})))
+        "Property pages correctly found for given class")
+    (is (= nil
+           (ldb/page-exists? @conn "foo" #{:logseq.class/Tag}))
+        "Property pages correctly not found for given class")
+    (is (= ["movie"]
+           (map #(:block/title (d/entity @conn %)) (ldb/page-exists? @conn "movie" #{:logseq.class/Tag})))
+        "Class pages correctly found for given class")
+    (is (= nil
+           (ldb/page-exists? @conn "movie" #{:logseq.class/Property}))
+        "Class pages correctly not found for given class")))

+ 10 - 5
deps/graph-parser/src/logseq/graph_parser/block.cljs

@@ -347,10 +347,11 @@
                   {:block/created-at current-ms
                    :block/updated-at current-ms}))
               (if journal-day
-                (cond-> {:block/type "journal"
-                         :block/journal-day journal-day}
+                (cond-> {:block/journal-day journal-day}
                   db-based?
-                  (assoc :block/tags [:logseq.class/Journal]))
+                  (assoc :block/tags [:logseq.class/Journal])
+                  (not db-based?)
+                  (assoc :block/type "journal"))
                 {}))]
     [page page-entity]))
 
@@ -388,8 +389,12 @@
                                                  nil)]
                                   [page nil]))]
       (when page
-        (let [type (if class? "class" (or (:block/type page) "page"))]
-          (assoc page :block/type type))))))
+        (if (ldb/db-based-graph? db)
+          (let [tags (if class? [:logseq.class/Tag]
+                         (or (:block/tags page)
+                             [:logseq.class/Page]))]
+            (assoc page :block/tags tags))
+          (assoc page :block/type (or (:block/type page) "page")))))))
 
 (defn- db-namespace-page?
   "Namespace page that're not journal pages"

+ 105 - 54
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -72,20 +72,23 @@
   ([db class-name all-idents]
    (find-or-create-class db class-name all-idents {}))
   ([db class-name all-idents class-block]
-   (if-let [db-ident (get @all-idents (keyword class-name))]
-     {:db/ident db-ident}
-     (let [m
-           (if (:block/namespace class-block)
-             ;; Give namespaced tags a unique ident so they don't conflict with other tags
-             (-> (db-class/build-new-class db {:block/title (build-class-ident-name class-name)})
-                 (merge {:block/title class-name
-                         :block/name (common-util/page-name-sanity-lc class-name)})
-                 (build-new-namespace-page))
-             (db-class/build-new-class db
-                                       {:block/title class-name
-                                        :block/name (common-util/page-name-sanity-lc class-name)}))]
-       (swap! all-idents assoc (keyword class-name) (:db/ident m))
-       (with-meta m {:new-class? true})))))
+   (let [ident (keyword class-name)]
+     (if-let [db-ident (get @all-idents ident)]
+       {:db/ident db-ident}
+       (let [m
+             (if (:block/namespace class-block)
+               ;; Give namespaced tags a unique ident so they don't conflict with other tags
+               (-> (db-class/build-new-class db (merge {:block/title (build-class-ident-name class-name)}
+                                                       (select-keys class-block [:block/tags])))
+                   (merge {:block/title class-name
+                           :block/name (common-util/page-name-sanity-lc class-name)})
+                   (build-new-namespace-page))
+               (db-class/build-new-class db
+                                         (assoc {:block/title class-name
+                                                 :block/name (common-util/page-name-sanity-lc class-name)}
+                                                :block/tags (:block/tags class-block))))]
+         (swap! all-idents assoc ident (:db/ident m))
+         (with-meta m {:new-class? true}))))))
 
 (defn- find-or-gen-class-uuid [page-names-to-uuids page-name db-ident & {:keys [temp-new-class?]}]
   (or (if temp-new-class?
@@ -115,7 +118,7 @@
   (if block-ns
     (->> (d/q '[:find [?b ...]
                 :in $ ?name
-                :where [?b :block/uuid ?uuid] [?b :block/type "class"] [?b :block/name ?name]]
+                :where [?b :block/uuid ?uuid] [?b :block/tags :logseq.class/Tag] [?b :block/name ?name]]
               db
               (ns-util/get-last-part full-name))
          (map #(d/entity db %))
@@ -127,7 +130,7 @@
     (first
      (d/q '[:find [?uuid ...]
             :in $ ?name
-            :where [?b :block/uuid ?uuid] [?b :block/type "class"] [?b :block/name ?name]]
+            :where [?b :block/uuid ?uuid] [?b :block/tags :logseq.class/Tag] [?b :block/name ?name]]
           db
           full-name))))
 
@@ -144,21 +147,30 @@
       (assert (:block/uuid class-m') "Class must have a :block/uuid")
       [:block/uuid (:block/uuid class-m')])
     (when (convert-tag? (:block/name tag-block) user-options)
-      (if-let [existing-tag-uuid (find-existing-class db tag-block)]
-        [:block/uuid existing-tag-uuid]
-        ;; Creates or updates page within same tx
-        (let [class-m (find-or-create-class db (:block/title tag-block) all-idents tag-block)
-              class-m' (-> (merge tag-block class-m
-                                  (when-not (:block/uuid tag-block)
-                                    {:block/uuid (find-or-gen-class-uuid page-names-to-uuids (:block/name tag-block) (:db/ident class-m))}))
-                           ;; override with imported timestamps
-                           (dissoc :block/created-at :block/updated-at)
-                           (merge (add-missing-timestamps
-                                   (select-keys tag-block [:block/created-at :block/updated-at])))
-                           (replace-namespace-with-parent page-names-to-uuids))]
-          (when (:new-class? (meta class-m)) (swap! classes-tx conj class-m'))
-          (assert (:block/uuid class-m') "Class must have a :block/uuid")
-          [:block/uuid (:block/uuid class-m')])))))
+      (let [existing-tag-uuid (find-existing-class db tag-block)
+            internal-tag-conflict? (contains? #{"tag" "property" "page" "journal" "asset"} (:block/name tag-block))]
+        (cond
+          ;; Don't overwrite internal tags
+          (and existing-tag-uuid (not internal-tag-conflict?))
+          [:block/uuid existing-tag-uuid]
+
+          :else
+          ;; Creates or updates page within same tx
+          (let [class-m (find-or-create-class db (:block/title tag-block) all-idents tag-block)
+                class-m' (-> (merge tag-block class-m
+                                    (if internal-tag-conflict?
+                                      {:block/uuid (common-uuid/gen-uuid :db-ident-block-uuid (:db/ident class-m))}
+                                      (when-not (:block/uuid tag-block)
+                                        (let [id (find-or-gen-class-uuid page-names-to-uuids (:block/name tag-block) (:db/ident class-m))]
+                                          {:block/uuid id}))))
+                             ;; override with imported timestamps
+                             (dissoc :block/created-at :block/updated-at)
+                             (merge (add-missing-timestamps
+                                     (select-keys tag-block [:block/created-at :block/updated-at])))
+                             (replace-namespace-with-parent page-names-to-uuids))]
+            (when (:new-class? (meta class-m)) (swap! classes-tx conj class-m'))
+            (assert (:block/uuid class-m') "Class must have a :block/uuid")
+            [:block/uuid (:block/uuid class-m')]))))))
 
 (defn- logseq-class-ident?
   [k]
@@ -188,10 +200,17 @@
                                       ;; Ignore new class tags from extract e.g. :logseq.class/Journal
                                       (logseq-class-ident? %)))
                          (map #(vector :block/uuid (get-page-uuid (:page-names-to-uuids per-file-state) (:block/name %) {:block %})))
-                         set)]
+                         set)
+          page-classes (into #{:logseq.class/Page} db-class/page-children-classes)]
       (cond-> block
         true
         (update :block/tags convert-tags-to-classes db per-file-state user-options all-idents)
+        ;; ensure pages are a Page
+        true
+        (update :block/tags (fn [tags]
+                              (if (seq (set/intersection (set tags) page-classes))
+                                tags
+                                (conj (vec tags) :logseq.class/Page))))
         (seq page-tags)
         (merge {:logseq.property/page-tags page-tags})))
     block))
@@ -297,9 +316,8 @@
                                            (date-time-util/int->journal-title date-int (common-config/get-date-formatter user-config)))]
                                (assoc page-m
                                       :block/uuid (common-uuid/gen-uuid :journal-page-uuid date-int)
-                                      :block/type "journal"
                                       :block/journal-day date-int)))
-                         (assoc :block/tags :logseq.class/Journal))]
+                         (assoc :block/tags #{:logseq.class/Journal}))]
       {:block
        (-> block
            (assoc :logseq.task/deadline [:block/uuid (:block/uuid deadline-page)])
@@ -699,8 +717,9 @@
              (seq classes-from-properties)
              ;; Add a map of {:block.temp/new-class TAG} to be processed later
              (update :block/tags
-                     (fnil into [])
-                     (map #(hash-map :block.temp/new-class %) classes-from-properties)))
+                     (fn [tags]
+                       (let [tags' (if (sequential? tags) tags (set tags))]
+                         (into tags' (map #(hash-map :block.temp/new-class %) classes-from-properties))))))
            :properties-tx pvalues-tx})
         {:block block :properties-tx []})
       (update :block dissoc :block/properties :block/properties-text-values :block/properties-order :block/invalid-properties)))
@@ -911,7 +930,7 @@
                 (:block/name %))
               (or (:block/uuid %)
                   (throw (ex-info (str "No uuid for existing page " (pr-str (:block/name %)))
-                                  (select-keys % [:block/name :block/type]))))))
+                                  (select-keys % [:block/name :block/tags]))))))
        (into {})))
 
 (defn- build-existing-page
@@ -919,13 +938,10 @@
   (let [;; These attributes are not allowed to be transacted because they must not change across files
         disallowed-attributes [:block/name :block/uuid :block/format :block/title :block/journal-day
                                :block/created-at :block/updated-at]
-        allowed-attributes (into [:block/tags :block/alias :logseq.property/parent :block/type :db/ident]
+        allowed-attributes (into [:block/tags :block/alias :logseq.property/parent :db/ident]
                                  (keep #(when (db-malli-schema/user-property? (key %)) (key %))
                                        m))
-        block-changes (cond-> (select-keys m allowed-attributes)
-                        ;; disallow any type -> "page" but do allow any conversion to a non-page type
-                        (ldb/internal-page? m)
-                        (dissoc :block/type))]
+        block-changes (select-keys m allowed-attributes)]
     (when-let [ignored-attrs (not-empty (apply dissoc m (into disallowed-attributes allowed-attributes)))]
       (notify-user {:msg (str "Import ignored the following attributes on page " (pr-str (:block/title m)) ": "
                               ignored-attrs)}))
@@ -956,8 +972,8 @@
             (assoc :block/uuid (d/squuid))
             ;; only happens for few file built-ins like tags and alias
             (and (contains? all-built-in-names (keyword (:block/name page)))
-                 (not (:block/type page)))
-            (assoc :block/type "page")))]
+                 (not (:block/tags page)))
+            (assoc :block/tags [:logseq.class/Page])))]
     (cond-> page'
       (:block/namespace page)
       ((fn [block']
@@ -997,7 +1013,7 @@
                                                          (all-existing-page-uuids (::original-name m))
                                                          (all-existing-page-uuids (:block/name m)))]
                                       (build-existing-page (dissoc m ::original-name ::original-title) @conn page-uuid per-file-state options)
-                                      (when (or (= "class" (:block/type m))
+                                      (when (or (ldb/class? m)
                                                 ;; Don't build a new page if it overwrites an existing class
                                                 (not (some-> (get @(:all-idents import-state)
                                                                   (some-> (or (::original-title m) (:block/title m))
@@ -1137,18 +1153,22 @@
                                                               (get-property-schema @(:property-schemas import-state) kw-name)
                                                               {:title (name kw-name)})]
                  (assert existing-page-uuid)
-                 (merge (select-keys new-prop [:block/type :block/schema :db/ident :db/index :db/cardinality :db/valueType])
+                 (merge (select-keys new-prop [:block/tags :block/schema :db/ident :db/index :db/cardinality :db/valueType])
                         {:block/uuid existing-page-uuid})))
              (set/intersection new-properties (set (map keyword (keys existing-pages)))))
+        ;; Could do this only for existing pages but the added complexity isn't worth reducing the tx noise
+        retract-page-tag-from-properties-tx (map #(vector :db/retract [:block/uuid (:block/uuid %)] :block/tags :logseq.class/Page)
+                                                  (concat property-pages-tx converted-property-pages-tx))
         ;; Save properties on new property pages separately as they can contain new properties and thus need to be
         ;; transacted separately the property pages
         property-page-properties-tx (keep (fn [b]
                                             (when-let [page-properties (not-empty (db-property/properties b))]
                                               (merge page-properties {:block/uuid (:block/uuid b)
-                                                                      :block/type "property"})))
+                                                                      :block/tags (-> (remove #(= :logseq.class/Page %) (:block/tags page-properties))
+                                                                                      (conj :logseq.class/Property))})))
                                           properties-tx)]
     {:pages-tx pages-tx'
-     :property-pages-tx (concat property-pages-tx converted-property-pages-tx)
+     :property-pages-tx (concat property-pages-tx converted-property-pages-tx retract-page-tag-from-properties-tx)
      :property-page-properties-tx property-page-properties-tx}))
 
 (defn- update-whiteboard-blocks [blocks format]
@@ -1219,8 +1239,9 @@
                                (->> pages
                                     ;; migrate previous attribute for :block/title
                                     (map #(-> %
-                                              (assoc :block/title (:block/original-name %))
-                                              (dissoc :block/original-name))))))
+                                              (assoc :block/title (or (:block/original-name %) (:block/title %))
+                                                     :block/tags #{:logseq.class/Whiteboard})
+                                              (dissoc :block/type :block/original-name))))))
               (update :blocks update-whiteboard-blocks format))
 
           :else
@@ -1234,6 +1255,32 @@
                [(:block/name %) (date-time-util/journal-day->ms journal-day)]))
        (into {})))
 
+(defn- clean-extra-invalid-tags
+  "If a page/class tx is an existing property or a new or existing class, ensure that
+  it only has one tag by removing :logseq.class/Page from its tx"
+  [db pages-tx' classes-tx]
+  ;; TODO: Improve perf if we tracked all created classes in atom
+  (let [existing-classes (->> (d/datoms db :avet :block/tags :logseq.class/Tag)
+                              (map #(d/entity db (:e %)))
+                              (map :block/uuid)
+                              set)
+        classes (set/union existing-classes
+                           (set (map :block/uuid classes-tx)))
+        existing-properties (->> (d/datoms db :avet :block/tags :logseq.class/Property)
+                                 (map #(d/entity db (:e %)))
+                                 (map :block/uuid)
+                                 set)]
+    {:pages-tx
+     (mapv (fn [page]
+             (if (or (contains? classes (:block/uuid page))
+                     (contains? existing-properties (:block/uuid page)))
+               (update page :block/tags (fn [tags] (vec (remove #(= % :logseq.class/Page) tags))))
+               page))
+           pages-tx')
+     :retract-page-tag-from-classes-tx
+     (mapv #(vector :db/retract [:block/uuid (:block/uuid %)] :block/tags :logseq.class/Page)
+                                              classes-tx)}))
+
 (defn add-file-to-db-graph
   "Parse file and save parsed data to the given db graph. Options available:
 
@@ -1271,12 +1318,15 @@
                        vec)
         {:keys [property-pages-tx property-page-properties-tx] pages-tx' :pages-tx}
         (split-pages-and-properties-tx pages-tx old-properties existing-pages (:import-state options))
+        ;; _ (when (seq property-pages-tx) (cljs.pprint/pprint {:property-pages-tx property-pages-tx}))
         ;; Necessary to transact new property entities first so that block+page properties can be transacted next
         main-props-tx-report (d/transact! conn property-pages-tx {::new-graph? true})
 
         classes-tx @(:classes-tx tx-options)
+        {:keys [retract-page-tag-from-classes-tx] pages-tx'' :pages-tx} (clean-extra-invalid-tags @conn pages-tx' classes-tx)
+        classes-tx' (concat classes-tx retract-page-tag-from-classes-tx)
         ;; Build indices
-        pages-index (->> (map #(select-keys % [:block/uuid]) pages-tx')
+        pages-index (->> (map #(select-keys % [:block/uuid]) pages-tx'')
                          (concat (map #(select-keys % [:block/uuid]) classes-tx))
                          distinct)
         block-ids (map (fn [block] {:block/uuid (:block/uuid block)}) blocks-tx)
@@ -1289,7 +1339,7 @@
         blocks-index (set/union (set block-ids) (set block-refs-ids))
         ;; Order matters. pages-index and blocks-index needs to come before their corresponding tx for
         ;; uuids to be valid. Also upstream-properties-tx comes after blocks-tx to possibly override blocks
-        tx (concat whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx' classes-tx blocks-index blocks-tx)
+        tx (concat whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx'' classes-tx' blocks-index blocks-tx)
         tx' (common-util/fast-remove-nils tx)
         ;; _ (prn :tx-counts (map count (vector whiteboard-pages pages-index page-properties-tx property-page-properties-tx pages-tx' classes-tx blocks-index blocks-tx)))
         ;; _ (when (not (seq whiteboard-pages)) (cljs.pprint/pprint {#_:property-pages-tx #_property-pages-tx :tx tx'}))
@@ -1297,6 +1347,7 @@
 
         upstream-properties-tx
         (build-upstream-properties-tx @conn @(:upstream-properties tx-options) (:import-state options) log-fn)
+        ;; _ (when (seq upstream-properties-tx) (cljs.pprint/pprint {:upstream-properties-tx upstream-properties-tx}))
         upstream-tx-report (when (seq upstream-properties-tx) (d/transact! conn upstream-properties-tx {::new-graph? true}))]
 
     ;; Return all tx-reports that occurred in this fn as UI needs to know what changed
@@ -1392,7 +1443,7 @@
 (defn- export-class-properties
   [conn repo-or-conn]
   (let [user-classes (->> (d/q '[:find (pull ?b [:db/id :db/ident])
-                                 :where [?b :block/type "class"]] @conn)
+                                 :where [?b :block/tags :logseq.class/Tag]] @conn)
                           (map first)
                           (remove #(db-class/built-in-classes (:db/ident %))))
         class-to-prop-uuids
@@ -1404,7 +1455,7 @@
                     [(contains? ?user-classes ?class)]
                     [?b ?prop _]
                     [?prop-e :db/ident ?prop]
-                    [?prop-e :block/type "property"]]
+                    [?prop-e :block/tags :logseq.class/Property]]
                   @conn
                   (set (map :db/ident user-classes)))
              (remove #(ldb/built-in? (d/entity @conn (second %))))

+ 1 - 2
deps/graph-parser/src/logseq/graph_parser/test/docs_graph_helper.cljs

@@ -23,7 +23,6 @@
     (sh ["git" "clone" "--depth" "1" "-b" branch "-c" "advice.detachedHead=false"
          "https://github.com/logseq/docs" dir] {})))
 
-
 ;; Fns for common test assertions
 ;; ==============================
 (defn get-top-block-properties
@@ -62,7 +61,7 @@
 (defn- get-journal-page-count [db]
   (->> (d/q '[:find (count ?b)
               :where
-              [?b :block/type "journal"]
+              [?b :block/journal-day]
               [?b :block/name]
               [?b :block/file]]
             db)

+ 122 - 107
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -27,23 +27,6 @@
 ;; =======
 ;; some have been copied from db-import script
 
-(defn- find-block-by-content [db content]
-  (if (instance? js/RegExp content)
-    (->> content
-         (d/q '[:find [(pull ?b [*]) ...]
-                :in $ ?pattern
-                :where [?b :block/title ?content]
-                [(missing? $ ?b :block/type)]
-                [(re-find ?pattern ?content)]]
-              db)
-         first)
-    (->> content
-         (d/q '[:find [(pull ?b [*]) ...]
-                :in $ ?content
-                :where [?b :block/title ?content] [(missing? $ ?b :block/type)]]
-              db)
-         first)))
-
 (defn- extract-rules
   [rules]
   (rules/extract-rules rules/db-query-dsl-rules
@@ -63,14 +46,6 @@
             db property property-value (extract-rules [:property]))
        first))
 
-(defn- find-page-by-name [db name]
-  (->> name
-       (d/q '[:find [(pull ?b [*]) ...]
-              :in $ ?name
-              :where [?b :block/title ?name]]
-            db)
-       first))
-
 (defn- build-graph-files
   "Given a file graph directory, return all files including assets and adds relative paths
    on ::rpath since paths are absolute by default and exporter needs relative paths for
@@ -177,7 +152,14 @@
 
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
         "Created graph has no validation errors")
-    (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties")))
+    (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties")
+    (is (= []
+             (->> (d/q '[:find (pull ?b [:block/title {:block/tags [:db/ident]}])
+                         :where [?b :block/tags :logseq.class/Tag]]
+                       @conn)
+                  (map first)
+                  (remove #(= [{:db/ident :logseq.class/Tag}] (:block/tags %)))))
+          "All classes only have :logseq.class/Tag as their tag (and don't have Page)")))
 
 (deftest-async export-basic-graph-with-convert-all-tags
   ;; This graph will contain basic examples of different features to import
@@ -195,25 +177,30 @@
 
       ;; Counts
       ;; Includes journals as property values e.g. :logseq.task/deadline
-      (is (= 24 (count (d/q '[:find ?b :where [?b :block/type "journal"]] @conn))))
       (is (= 24 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Journal]] @conn))))
 
       (is (= 4 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Task]] @conn))))
       (is (= 3 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Query]] @conn))))
       (is (= 2 (count (d/q '[:find ?b :where [?b :block/tags :logseq.class/Card]] @conn))))
 
-      ;; Don't count pages like url.md that have properties but no content
+      ;; Properties and tags aren't included in this count as they aren't a Page
       (is (= 10
-             (count (->> (d/q '[:find [(pull ?b [:block/title :block/type]) ...]
-                                :where [?b :block/title] [_ :block/page ?b] (not [?b :logseq.property/built-in?])] @conn)
-                         (filter ldb/internal-page?))))
+             (->> (d/q '[:find [?b ...]
+                         :where
+                         [?b :block/title]
+                         [_ :block/page ?b]
+                         (not [?b :logseq.property/built-in?])] @conn)
+                  (map #(d/entity @conn %))
+                  (filter ldb/internal-page?)
+                  #_(map #(select-keys % [:block/title :block/tags]))
+                  count))
           "Correct number of pages with block content")
       (is (= 11 (->> @conn
                      (d/q '[:find [?ident ...]
-                            :where [?b :block/type "class"] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
+                            :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
                      count))
           "Correct number of user classes")
-      (is (= 4 (count (d/datoms @conn :avet :block/type "whiteboard"))))
+      (is (= 4 (count (d/datoms @conn :avet :block/tags :logseq.class/Whiteboard))))
       (is (= 0 (count @(:ignored-properties import-state))) ":filters should be the only ignored property")
       (is (= 1 (count @assets))))
 
@@ -236,7 +223,7 @@
       (is (= 18
              (->> @conn
                   (d/q '[:find [(pull ?b [:db/ident]) ...]
-                         :where [?b :block/type "property"]])
+                         :where [?b :block/tags :logseq.class/Property]])
                   (remove #(db-malli-schema/internal-ident? (:db/ident %)))
                   count))
           "Correct number of user properties")
@@ -248,7 +235,7 @@
                {:db/ident :user.property/startedat :block/schema {:type :date}}}
              (->> @conn
                   (d/q '[:find [(pull ?b [:db/ident :block/schema]) ...]
-                         :where [?b :block/type "property"]])
+                         :where [?b :block/tags :logseq.class/Property]])
                   (filter #(contains? #{:prop-bool :prop-string :prop-num :rangeincludes :sameas :startedat}
                                       (keyword (name (:db/ident %)))))
                   set))
@@ -263,45 +250,55 @@
       (is (= {:user.property/prop-bool true
               :user.property/prop-num 5
               :user.property/prop-string "woot"}
-             (update-vals (db-property/properties (find-block-by-content @conn "b1"))
+             (update-vals (db-property/properties (db-test/find-block-by-content @conn "b1"))
                           (fn [v] (if (map? v) (db-property/ref->property-value-content @conn v) v))))
           "Basic block has correct properties")
       (is (= #{"prop-num" "prop-string" "prop-bool"}
-             (->> (d/entity @conn (:db/id (find-block-by-content @conn "b1")))
+             (->> (d/entity @conn (:db/id (db-test/find-block-by-content @conn "b1")))
                   :block/refs
                   (map :block/title)
                   set))
           "Block with properties has correct refs")
 
-      (is (= {:user.property/prop-num2 10}
-             (readable-properties @conn (find-page-by-name @conn "new page")))
+      (is (= {:user.property/prop-num2 10
+              :block/tags [:logseq.class/Page]}
+             (readable-properties @conn (db-test/find-page-by-title @conn "new page")))
           "New page has correct properties")
       (is (= {:user.property/prop-bool true
               :user.property/prop-num 5
-              :user.property/prop-string "yeehaw"}
-             (readable-properties @conn (find-page-by-name @conn "some page")))
+              :user.property/prop-string "yeehaw"
+              :block/tags [:logseq.class/Page]}
+             (readable-properties @conn (db-test/find-page-by-title @conn "some page")))
           "Existing page has correct properties")
 
       (is (= {:user.property/rating 5.5}
-             (readable-properties @conn (find-block-by-content @conn ":rating float")))
-          "Block with float property imports as a float"))
+             (readable-properties @conn (db-test/find-block-by-content @conn ":rating float")))
+          "Block with float property imports as a float")
+
+      (is (= []
+             (->> (d/q '[:find (pull ?b [:block/title {:block/tags [:db/ident]}])
+                         :where [?b :block/tags :logseq.class/Property]]
+                       @conn)
+                  (map first)
+                  (remove #(= [{:db/ident :logseq.class/Property}] (:block/tags %)))))
+          "All properties only have :logseq.class/Property as their tag (and don't have Page)"))
 
     (testing "built-in properties"
-      (is (= [(:db/id (find-block-by-content @conn "original block"))]
-             (mapv :db/id (:block/refs (find-block-by-content @conn #"ref to"))))
+      (is (= [(:db/id (db-test/find-block-by-content @conn "original block"))]
+             (mapv :db/id (:block/refs (db-test/find-block-by-content @conn #"ref to"))))
           "block with a block-ref has correct :block/refs")
 
-      (let [b (find-block-by-content @conn #"MEETING TITLE")]
+      (let [b (db-test/find-block-by-content @conn #"MEETING TITLE")]
         (is (= {}
                (and b (readable-properties @conn b)))
             ":template properties are ignored to not invalidate its property types"))
 
       (is (= {:logseq.task/deadline "Nov 26th, 2022"}
-             (readable-properties @conn (find-block-by-content @conn "only deadline")))
+             (readable-properties @conn (db-test/find-block-by-content @conn "only deadline")))
           "deadline block has correct journal as property value")
 
       (is (= {:logseq.task/deadline "Nov 25th, 2022"}
-             (readable-properties @conn (find-block-by-content @conn "only scheduled")))
+             (readable-properties @conn (db-test/find-block-by-content @conn "only scheduled")))
           "scheduled block converted to correct deadline")
 
       (is (= 1 (count (d/q '[:find [(pull ?b [*]) ...]
@@ -311,28 +308,36 @@
           "Only one journal page exists when deadline is on same day as journal")
 
       (is (= {:logseq.task/priority "High"}
-             (readable-properties @conn (find-block-by-content @conn "high priority")))
+             (readable-properties @conn (db-test/find-block-by-content @conn "high priority")))
           "priority block has correct property")
 
       (is (= {:logseq.task/status "Doing" :logseq.task/priority "Medium" :block/tags [:logseq.class/Task]}
-             (readable-properties @conn (find-block-by-content @conn "status test")))
+             (readable-properties @conn (db-test/find-block-by-content @conn "status test")))
           "status block has correct task properties and class")
 
       (is (= #{:logseq.task/status :block/tags}
-             (set (keys (readable-properties @conn (find-block-by-content @conn "old todo block")))))
+             (set (keys (readable-properties @conn (db-test/find-block-by-content @conn "old todo block")))))
           "old task properties like 'todo' are ignored")
 
       (is (= {:logseq.property/order-list-type "number"}
-             (readable-properties @conn (find-block-by-content @conn "list one")))
+             (readable-properties @conn (db-test/find-block-by-content @conn "list one")))
           "numered block has correct property")
 
       (is (= #{"gpt"}
-             (:block/alias (readable-properties @conn (find-page-by-name @conn "chat-gpt"))))
+             (:block/alias (readable-properties @conn (db-test/find-page-by-title @conn "chat-gpt"))))
           "alias set correctly")
+      (is (= ["y"]
+             (->> (d/q '[:find [?b ...] :where [?b :block/title "y"] [?b :logseq.property/parent]]
+                       @conn)
+                  first
+                  (d/entity @conn)
+                  :block/alias
+                  (map :block/title)))
+          "alias set correctly on namespaced page")
 
       (is (= {:logseq.property.linked-references/includes #{"Oct 9th, 2024"}
               :logseq.property.linked-references/excludes #{"ref2"}}
-             (select-keys (readable-properties @conn (find-page-by-name @conn "chat-gpt"))
+             (select-keys (readable-properties @conn (db-test/find-page-by-title @conn "chat-gpt"))
                           [:logseq.property.linked-references/excludes :logseq.property.linked-references/includes]))
           "linked ref filters set correctly"))
 
@@ -346,36 +351,44 @@
              (readable-properties @conn (find-block-by-property-value @conn :logseq.property/query "(property :prop-string)")))
           "simple query block has correct query properties")
       (is (= "For example, here's a query with title text:"
-             (:block/title (find-block-by-content @conn #"query with title text")))
+             (:block/title (db-test/find-block-by-content @conn #"query with title text")))
           "Text around a simple query block is set as a query's title")
       (is (= {:logseq.property.view/type "List View"
               :logseq.property/query "{:query (task todo doing)}"
               :block/tags [:logseq.class/Query]
               :logseq.property.table/ordered-columns [:block/title]}
-             (readable-properties @conn (find-block-by-content @conn #"tasks with")))
+             (readable-properties @conn (db-test/find-block-by-content @conn #"tasks with")))
           "Advanced query has correct query properties")
       (is (= "tasks with todo and doing"
-             (:block/title (find-block-by-content @conn #"tasks with")))
+             (:block/title (db-test/find-block-by-content @conn #"tasks with")))
           "Advanced query has custom title migrated")
 
       ;; Cards
       (is (= {:block/tags [:logseq.class/Card]}
-             (readable-properties @conn (find-block-by-content @conn "card 1")))
+             (readable-properties @conn (db-test/find-block-by-content @conn "card 1")))
           "None of the card properties are imported since they are deprecated"))
 
     (testing "tags convert to classes"
       (is (= :user.class/Quotes___life
-             (:db/ident (find-page-by-name @conn "life")))
+             (:db/ident (db-test/find-page-by-title @conn "life")))
           "Namespaced tag's ident has hierarchy to make it unique")
 
-      (is (= [{:block/type "class"}]
-             (d/q '[:find [(pull ?b [:block/type]) ...] :where [?b :block/name "life"]] @conn))
+      (is (= [:logseq.class/Tag]
+             (map :db/ident (:block/tags (db-test/find-page-by-title @conn "life"))))
           "When a class is used and referenced on the same page, there should only be one instance of it")
 
       (is (= ["life"]
-             (->> (:block/tags (find-block-by-content @conn #"with namespace tag"))
+             (->> (:block/tags (db-test/find-block-by-content @conn #"with namespace tag"))
                   (mapv #(db-property/ref->property-value-contents @conn %))))
-          "Block tagged with namespace tag is only associated with leaf child tag"))
+          "Block tagged with namespace tag is only associated with leaf child tag")
+
+      (is (= []
+             (->> (d/q '[:find (pull ?b [:block/title {:block/tags [:db/ident]}])
+                         :where [?b :block/tags :logseq.class/Tag]]
+                       @conn)
+                  (map first)
+                  (remove #(= [{:db/ident :logseq.class/Tag}] (:block/tags %)))))
+          "All classes only have :logseq.class/Tag as their tag (and don't have Page)"))
 
     (testing "namespaces"
       (let [expand-children (fn expand-children [ent parent]
@@ -386,25 +399,25 @@
         (is (= [{:parent "n1" :child "x"}
                 {:parent "x" :child "z"}
                 {:parent "x" :child "y"}]
-               (rest (expand-children (d/entity @conn (:db/id (find-page-by-name @conn "n1"))) nil)))
+               (rest (expand-children (d/entity @conn (:db/id (db-test/find-page-by-title @conn "n1"))) nil)))
             "First namespace tests duplicate parent page name")
         (is (= [{:parent "n2" :child "x"}
                 {:parent "x" :child "z"}
                 {:parent "n2" :child "alias"}]
-               (rest (expand-children (d/entity @conn (:db/id (find-page-by-name @conn "n2"))) nil)))
+               (rest (expand-children (d/entity @conn (:db/id (db-test/find-page-by-title @conn "n2"))) nil)))
             "First namespace tests duplicate child page name and built-in page name")))
 
     (testing "journal timestamps"
       (is (= (date-time-util/journal-day->ms 20240207)
-             (:block/created-at (find-page-by-name @conn "Feb 7th, 2024")))
+             (:block/created-at (db-test/find-page-by-title @conn "Feb 7th, 2024")))
           "journal pages are created on their journal day")
       (is (= (date-time-util/journal-day->ms 20240207)
-             (:block/created-at (find-block-by-content @conn #"Inception")))
+             (:block/created-at (db-test/find-block-by-content @conn #"Inception")))
           "journal blocks are created on their page's journal day"))
 
     (testing "db attributes"
       (is (= true
-             (:block/collapsed? (find-block-by-content @conn "collapsed block")))
+             (:block/collapsed? (db-test/find-block-by-content @conn "collapsed block")))
           "Collapsed blocks are imported"))
 
     (testing "property :type changes"
@@ -419,7 +432,7 @@
              (get-in (d/entity @conn :user.property/description) [:block/schema :type]))
           ":default property to :node (or any non :default value) remains :default")
       (is (= "[[Jakob]]"
-             (:user.property/description (readable-properties @conn (find-block-by-content @conn #":default to :node"))))
+             (:user.property/description (readable-properties @conn (db-test/find-block-by-content @conn #":default to :node"))))
           ":default to :node property saves :default property value default with full text")
 
       (testing "with changes to upstream/existing property value"
@@ -427,19 +440,19 @@
                (get-in (d/entity @conn :user.property/duration) [:block/schema :type]))
             ":number property to :default value changes to :default")
         (is (= "20"
-               (:user.property/duration (readable-properties @conn (find-block-by-content @conn "existing :number to :default"))))
+               (:user.property/duration (readable-properties @conn (db-test/find-block-by-content @conn "existing :number to :default"))))
             "existing :number property value correctly saved as :default")
 
         (is (= {:block/schema {:type :default} :db/cardinality :db.cardinality/many}
                (select-keys (d/entity @conn :user.property/people) [:block/schema :db/cardinality]))
             ":node property to :default value changes to :default and keeps existing cardinality")
         (is (= #{"[[Jakob]] [[Gabriel]]"}
-               (:user.property/people (readable-properties @conn (find-block-by-content @conn ":node people"))))
+               (:user.property/people (readable-properties @conn (db-test/find-block-by-content @conn ":node people"))))
             "existing :node property value correctly saved as :default with full text")
         (is (= #{"[[Gabriel]] [[Jakob]]"}
-               (:user.property/people (readable-properties @conn (find-block-by-content @conn #"pending block for :node"))))
+               (:user.property/people (readable-properties @conn (db-test/find-block-by-content @conn #"pending block for :node"))))
             "pending :node property value correctly saved as :default with full text")
-        (is (some? (find-page-by-name @conn "Jakob"))
+        (is (some? (db-test/find-page-by-title @conn "Jakob"))
             "Previous :node property value still exists")
         (is (= 3 (count (find-block-by-property @conn :user.property/people)))
             "Converted property has correct number of property values")))
@@ -448,17 +461,20 @@
       (is (= #{:logseq.property/description :user.property/description}
              (set (d/q '[:find [?ident ...] :where [?b :db/ident ?ident] [?b :block/name "description"]] @conn)))
           "user description property is separate from built-in one")
-      (is (= #{"page" "class"}
-             (set (d/q '[:find [?type ...] :where [?b :block/type ?type] [?b :block/name "task"]] @conn)))
+      (is (= #{"Page" "Tag"}
+             (set (d/q '[:find [?t-title ...] :where
+                         [?b :block/tags ?t]
+                         [?b :block/name "task"]
+                         [?t :block/title ?t-title]] @conn)))
           "user page is separate from built-in class"))
 
     (testing "multiline blocks"
-      (is (= "|markdown| table|\n|some|thing|" (:block/title (find-block-by-content @conn #"markdown.*table"))))
-      (is (= "multiline block\na 2nd\nand a 3rd" (:block/title (find-block-by-content @conn #"multiline block"))))
-      (is (= "logbook block" (:block/title (find-block-by-content @conn #"logbook block")))))
+      (is (= "|markdown| table|\n|some|thing|" (:block/title (db-test/find-block-by-content @conn #"markdown.*table"))))
+      (is (= "multiline block\na 2nd\nand a 3rd" (:block/title (db-test/find-block-by-content @conn #"multiline block"))))
+      (is (= "logbook block" (:block/title (db-test/find-block-by-content @conn #"logbook block")))))
 
     (testing ":block/refs and :block/path-refs"
-      (let [page (find-page-by-name @conn "chat-gpt")]
+      (let [page (db-test/find-page-by-title @conn "chat-gpt")]
         (is (set/subset?
              #{"type" "LargeLanguageModel"}
              (->> page :block/refs (map #(:block/title (d/entity @conn (:db/id %)))) set))
@@ -468,7 +484,7 @@
              (->> page :block/path-refs (map #(:block/title (d/entity @conn (:db/id %)))) set))
             "Page has correct property and property value :block/path-refs"))
 
-      (let [block (find-block-by-content @conn "old todo block")]
+      (let [block (db-test/find-block-by-content @conn "old todo block")]
         (is (set/subset?
              #{:logseq.task/status :logseq.class/Task}
              (->> block
@@ -485,7 +501,7 @@
             "Block has correct task tag and property :block/path-refs")))
 
     (testing "whiteboards"
-      (let [block-with-props (find-block-by-content @conn #"block with props")]
+      (let [block-with-props (db-test/find-block-by-content @conn #"block with props")]
         (is (= {:user.property/prop-num 10}
                (readable-properties @conn block-with-props)))
         (is (= "block with props" (:block/title block-with-props)))))))
@@ -501,7 +517,7 @@
     (is (= 0 (count @(:ignored-properties import-state))) "No ignored properties")
     (is (= 0 (->> @conn
                   (d/q '[:find [?ident ...]
-                         :where [?b :block/type "class"] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
+                         :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
                   count))
         "Correct number of user classes")
 
@@ -511,7 +527,7 @@
 
     (testing "replacing refs in :block/title when :remove-inline-tags? set"
       (is (= 2
-             (->> (find-block-by-content @conn #"replace with same start string")
+             (->> (db-test/find-block-by-content @conn #"replace with same start string")
                   :block/title
                   (re-seq db-content/id-ref-pattern)
                   distinct
@@ -519,7 +535,7 @@
           "A block with ref names that start with same string has 2 distinct refs")
 
       (is (= 1
-             (->> (find-block-by-content @conn #"replace case insensitive")
+             (->> (db-test/find-block-by-content @conn #"replace case insensitive")
                   :block/title
                   (re-seq db-content/id-ref-pattern)
                   distinct
@@ -527,9 +543,9 @@
           "A block with different case of same ref names has 1 distinct ref"))
 
     (testing "tags convert to page, refs and page-tags"
-      (let [block (find-block-by-content @conn #"Inception")
-            tag-page (find-page-by-name @conn "Movie")
-            tagged-page (find-page-by-name @conn "Interstellar")]
+      (let [block (db-test/find-block-by-content @conn #"Inception")
+            tag-page (db-test/find-page-by-title @conn "Movie")
+            tagged-page (db-test/find-page-by-title @conn "Interstellar")]
         (is (string/starts-with? (str (:block/title block)) "Inception [[")
             "tagged block tag converts tag to page ref")
         (is (= [(:db/id tag-page)] (map :db/id (:block/refs block)))
@@ -537,11 +553,11 @@
         (is (and tag-page (not (ldb/class? tag-page)))
             "tag page is not a class")
 
-        (is (= {:logseq.property/page-tags #{"Movie"}}
-               (readable-properties @conn tagged-page))
+        (is (= #{"Movie"}
+               (:logseq.property/page-tags (readable-properties @conn tagged-page)))
             "tagged page has existing page imported as a tag to page-tags")
         (is (= #{"LargeLanguageModel" "fun" "ai"}
-               (:logseq.property/page-tags (readable-properties @conn (find-page-by-name @conn "chat-gpt"))))
+               (:logseq.property/page-tags (readable-properties @conn (db-test/find-page-by-title @conn "chat-gpt"))))
             "tagged page has new page and other pages marked with '#' and '[[]]` imported as tags to page-tags")))))
 
 (deftest-async export-files-with-tag-classes-option
@@ -549,26 +565,25 @@
           files (mapv #(node-path/join file-graph-dir %) ["journals/2024_02_07.md" "pages/Interstellar.md"])
           conn (db-test/create-conn)
           _ (import-files-to-db files conn {:tag-classes ["movie"]})]
-
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
         "Created graph has no validation errors")
 
-    (let [block (find-block-by-content @conn #"Inception")
-          tag-page (find-page-by-name @conn "Movie")
-          another-tag-page (find-page-by-name @conn "p0")]
+    (let [block (db-test/find-block-by-content @conn #"Inception")
+          tag-page (db-test/find-page-by-title @conn "Movie")
+          another-tag-page (db-test/find-page-by-title @conn "p0")]
       (is (= (:block/title block) "Inception")
           "tagged block with configured tag strips tag from content")
       (is (= [:user.class/Movie]
              (:block/tags (readable-properties @conn block)))
           "tagged block has configured tag imported as a class")
 
-      (is (= "class" (:block/type tag-page))
+      (is (= [:logseq.class/Tag] (mapv :db/ident (:block/tags tag-page)))
           "configured tag page in :tag-classes is a class")
       (is (and another-tag-page (not (ldb/class? another-tag-page)))
           "unconfigured tag page is not a class")
 
-      (is (= {:block/tags [:user.class/Movie]}
-             (readable-properties @conn (find-page-by-name @conn "Interstellar")))
+      (is (= {:block/tags [:logseq.class/Page :user.class/Movie]}
+             (readable-properties @conn (db-test/find-page-by-title @conn "Interstellar")))
           "tagged page has configured tag imported as a class"))))
 
 (deftest-async export-files-with-property-classes-option
@@ -586,7 +601,7 @@
     (is (= #{:user.class/Property :user.class/Movie :user.class/Class :user.class/Tool}
            (->> @conn
                 (d/q '[:find [?ident ...]
-                       :where [?b :block/type "class"] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
+                       :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
                 set))
         "All classes are correctly defined by :type")
 
@@ -597,8 +612,8 @@
                 set))
         "Properties are correctly inferred for a class")
 
-    (let [block (find-block-by-content @conn #"The Creator")
-          tag-page (find-page-by-name @conn "Movie")]
+    (let [block (db-test/find-block-by-content @conn #"The Creator")
+          tag-page (db-test/find-page-by-title @conn "Movie")]
       (is (= (:block/title block) "The Creator")
           "tagged block with configured tag strips tag from content")
       (is (= [:user.class/Movie]
@@ -608,14 +623,14 @@
           "tagged block can have another property that references the same class it is tagged with,
            without creating a duplicate class")
 
-      (is (= "class" (:block/type tag-page))
+      (is (= [:logseq.class/Tag] (map :db/ident (:block/tags tag-page)))
           "configured tag page derived from :property-classes is a class")
-      (is (nil? (find-page-by-name @conn "type"))
+      (is (nil? (db-test/find-page-by-title @conn "type"))
           "No page exists for configured property")
 
-      (is (= [:user.class/Property]
-             (:block/tags (readable-properties @conn (find-page-by-name @conn "url"))))
-          "tagged page has configured tag imported as a class"))))
+      (is (= #{:user.class/Property :logseq.class/Property}
+             (set (:block/tags (readable-properties @conn (db-test/find-page-by-title @conn "url")))))
+          "tagged page has correct tags including one from option"))))
 
 (deftest-async export-files-with-remove-inline-tags
   (p/let [file-graph-dir "test/resources/exporter-test-graph"
@@ -625,7 +640,7 @@
 
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
         "Created graph has no validation errors")
-    (is (string/starts-with? (:block/title (find-block-by-content @conn #"Inception"))
+    (is (string/starts-with? (:block/title (db-test/find-block-by-content @conn #"Inception"))
                              "Inception #Movie")
         "block with tag preserves inline tag")))
 
@@ -654,7 +669,7 @@
              :user.class/Class :user.class/Tool :user.class/Whiteboard___Tool}
            (->> @conn
                 (d/q '[:find [?ident ...]
-                       :where [?b :block/type "class"] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
+                       :where [?b :block/tags :logseq.class/Tag] [?b :db/ident ?ident] (not [?b :logseq.property/built-in?])])
                 set))
         "All classes are correctly defined by :type")
 

+ 2 - 3
deps/graph-parser/test/logseq/graph_parser/extract_test.cljs

@@ -2,8 +2,7 @@
   (:require [cljs.test :refer [deftest is are]]
             [logseq.graph-parser.extract :as extract]
             [datascript.core :as d]
-            [logseq.db.frontend.schema :as db-schema]
-            [logseq.db :as ldb]))
+            [logseq.db.frontend.schema :as db-schema]))
 
 ;; This is a copy of frontend.util.fs/multiplatform-reserved-chars for reserved chars testing
 (def multiplatform-reserved-chars ":\\*\\?\"<>|\\#\\\\")
@@ -145,6 +144,6 @@
         page (first pages)]
     (is (= (get-in page [:block/file :file/path]) "/whiteboards/foo.edn"))
     (is (= (:block/name page) "foo"))
-    (is (ldb/whiteboard? page))
+    (is (= (:block/type page) "whiteboard"))
     (is (= (:block/title page) "Foo"))
     (is (every? #(= (:block/parent %) [:block/uuid #uuid "a846e3b4-c41d-4251-80e1-be6978c36d8c"]) blocks))))

+ 2 - 1
deps/outliner/src/logseq/outliner/core.cljs

@@ -80,7 +80,8 @@
                                                                       (let [refs (:block/_refs page)]
                                                                         (and (or (zero? (count refs))
                                                                                  (= #{db-id} (set (map :db/id refs))))
-                                                                             (not (some #{"class" "property"} (:block/type page))))))}))]
+                                                                             (not (ldb/class? page))
+                                                                             (not (ldb/property? page)))))}))]
       (when (seq orphaned-pages)
         (let [tx (mapv (fn [page] [:db/retractEntity (:db/id page)]) orphaned-pages)]
           (swap! txs-state (fn [state] (vec (concat state tx)))))))))

+ 12 - 3
deps/outliner/src/logseq/outliner/property.cljs

@@ -38,7 +38,7 @@
            multiple-values-empty? (and (sequential? old-value)
                                        (contains? (set (map :db/ident old-value)) :logseq.property/empty-placeholder))
            block' (assoc (outliner-core/block-with-updated-at {:db/id (:db/id block)})
-                        property-id value)
+                         property-id value)
            block-tx-data (cond-> block'
                            (and status? (not (ldb/class-instance? (d/entity @conn :logseq.class/Task) block)))
                            (assoc :block/tags :logseq.class/Task))]
@@ -160,6 +160,8 @@
             db-ident' (db-ident/ensure-unique-db-ident @conn db-ident)]
         (assert (some? k-name)
                 (prn "property-id: " property-id ", property-name: " property-name))
+        (outliner-validate/validate-page-title k-name {:node {:db/ident db-ident'}})
+        (outliner-validate/validate-page-title-characters k-name {:node {:db/ident db-ident'}})
         (ldb/transact! conn
                        [(sqlite-util/build-new-property db-ident' schema {:title k-name})]
                        {:outliner-op :new-property})
@@ -281,10 +283,16 @@
         _ (assert (qualified-keyword? property-id) "property-id should be a keyword")
         block (d/entity @conn block-eid)
         db-attribute? (some? (db-schema/schema-for-db-based-graph property-id))]
-    (if db-attribute?
+    (when (= property-id :block/tags)
+      (outliner-validate/validate-tags-property @conn [block-eid] v))
+    (when (= property-id :logseq.property/parent)
+      (outliner-validate/validate-parent-property v [block]))
+    (cond
+      db-attribute?
       (when-not (and (= property-id :block/alias) (= v (:db/id block))) ; alias can't be itself
         (ldb/transact! conn [{:db/id (:db/id block) property-id v}]
                        {:outliner-op :save-block}))
+      :else
       (let [property (d/entity @conn property-id)
             _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
             property-type (get-in property [:block/schema :type] :default)
@@ -302,6 +310,8 @@
   (assert property-id "property-id is nil")
   (throw-error-if-read-only-property property-id)
   (let [block-eids (map ->eid block-ids)
+        _ (when (= property-id :block/tags)
+            (outliner-validate/validate-tags-property @conn block-eids v))
         property (d/entity @conn property-id)
         _ (when (= (:db/ident property) :logseq.property/parent)
             (outliner-validate/validate-parent-property
@@ -553,7 +563,6 @@
           (when (seq values)
             (let [value-property-tx (map (fn [id]
                                            {:db/id id
-                                            :block/type "closed value"
                                             :block/closed-value-property (:db/id property)})
                                          (map :db/id values))
                   property-tx (outliner-core/block-with-updated-at {:db/id (:db/id property)})]

+ 97 - 67
deps/outliner/src/logseq/outliner/validate.cljs

@@ -1,11 +1,13 @@
 (ns logseq.outliner.validate
-  "Reusable DB graph validations for outliner level and above. Most validations throw
-  errors so the user action stops immediately to display a notification"
+  "Reusable DB graph validations for outliner level and above. Most validations
+  throw errors so the user action stops immediately to display a notification"
   (:require [clojure.string :as string]
             [datascript.core :as d]
             [logseq.db :as ldb]
             [logseq.common.date :as common-date]
-            [logseq.common.util.namespace :as ns-util]))
+            [logseq.common.util.namespace :as ns-util]
+            [clojure.set :as set]
+            [logseq.db.frontend.class :as db-class]))
 
 (defn ^:api validate-page-title-characters
   "Validates characters that must not be in a page title"
@@ -42,30 +44,6 @@
                      :payload {:message "Built-in pages can't be edited"
                                :type :warning}}))))
 
-(defn- validate-unique-for-property-page
-  [entity db new-title]
-  (when-let [_res (seq (d/q (if (:logseq.property/built-in? entity)
-                              '[:find [?b ...]
-                                :in $ ?eid ?title
-                                :where
-                                [?b :block/title ?title]
-                                [?b :block/type "property"]
-                                [(not= ?b ?eid)]]
-                              '[:find [?b ...]
-                                :in $ ?eid ?title
-                                :where
-                                [?b :block/title ?title]
-                                [?b :block/type "property"]
-                                [(missing? $ ?b :logseq.property/built-in?)]
-                                [(not= ?b ?eid)]])
-                            db
-                            (:db/id entity)
-                            new-title))]
-    (throw (ex-info "Duplicate property"
-                    {:type :notification
-                     :payload {:message (str "Another property named " (pr-str new-title) " already exists")
-                               :type :warning}}))))
-
 (defn- validate-unique-by-parent-and-name [db entity new-title]
   (when-let [_res (seq (d/q '[:find [?b ...]
                               :in $ ?eid ?type ?title
@@ -88,50 +66,49 @@
 (defn- validate-unique-for-page
   [db new-title {:block/keys [tags] :as entity}]
   (cond
-    (and (seq tags) (ldb/internal-page? entity))
-    (when-let [res (seq (d/q '[:find [?b ...]
-                               :in $ ?eid ?title [?tag-id ...]
-                               :where
-                               [?b :block/title ?title]
-                               [?b :block/tags ?tag-id]
-                               [(not= ?b ?eid)]]
-                             db
-                             (:db/id entity)
-                             new-title
-                             (map :db/id tags)))]
-      (throw (ex-info "Duplicate page by tag"
-                      {:type :notification
-                       :payload {:message (str "Another page named " (pr-str new-title) " already exists for tag "
-                                               (pr-str (->> res first (d/entity db) :block/tags first :block/title)))
-                                 :type :warning}})))
-
-    (ldb/property? entity)
-    (validate-unique-for-property-page entity db new-title)
+    (seq tags)
+    (when-let [another-id (first
+                           (d/q (if (ldb/property? entity)
+                                  ;; Property names are unique in that they can
+                                  ;; have the same names as built-in property names
+                                  '[:find [?b ...]
+                                    :in $ ?eid ?title [?tag-id ...]
+                                    :where
+                                    [?b :block/title ?title]
+                                    [?b :block/tags ?tag-id]
+                                    [(missing? $ ?b :logseq.property/built-in?)]
+                                    [(not= ?b ?eid)]]
+                                  '[:find [?b ...]
+                                    :in $ ?eid ?title [?tag-id ...]
+                                    :where
+                                    [?b :block/title ?title]
+                                    [?b :block/tags ?tag-id]
+                                    [(not= ?b ?eid)]])
+                                db
+                                (:db/id entity)
+                                new-title
+                                (map :db/id tags)))]
+      (let [another (d/entity db another-id)
+            this-tags (set (map :db/ident tags))
+            another-tags (set (map :db/ident (:block/tags another)))
+            common-tag-ids (set/intersection this-tags another-tags)]
+        (when-not (and (= common-tag-ids #{:logseq.class/Page})
+                       (> (count this-tags) 1)
+                       (> (count another-tags) 1))
+          (throw (ex-info "Duplicate page"
+                          {:type :notification
+                           :payload {:message (str "Another page named " (pr-str new-title) " already exists for tags: "
+                                                   (string/join ", "
+                                                                (map (fn [id] (str "#" (:block/title (d/entity db id)))) common-tag-ids)))
+                                     :type :warning}})))))
 
     (:logseq.property/parent entity)
-    (validate-unique-by-parent-and-name db entity new-title)
-
-    :else
-    (when-let [_res (seq (d/q '[:find [?b ...]
-                                :in $ ?eid ?type ?title
-                                :where
-                                [?b :block/title ?title]
-                                [?b :block/type ?type]
-                                [(not= ?b ?eid)]]
-                              db
-                              (:db/id entity)
-                              (:block/type entity)
-                              new-title))]
-      (throw (ex-info "Duplicate page without tag"
-                      {:type :notification
-                       :payload {:message (str "Another page named " (pr-str new-title) " already exists")
-                                 :type :warning}})))))
+    (validate-unique-by-parent-and-name db entity new-title)))
 
 (defn ^:api validate-unique-by-name-tag-and-block-type
   "Validates uniqueness of nodes for the following cases:
-   - Page names of type 'page' are unique by tag e.g. their can be Apple #Company and Apple #Fruit
-   - Page names of other types are unique for their type e.g. their can be #Journal ('class') and Journal ('page')
-   - Property names are unique and don't consider built-in property names"
+   - Page names are unique for a tag e.g. their can be Apple #Company and Apple #Fruit
+   - Page names are unique for a :logseq.property/parent"
   [db new-title entity]
   (when (ldb/page? entity)
     (validate-unique-for-page db new-title entity)))
@@ -153,7 +130,7 @@
   (validate-unique-by-name-tag-and-block-type db new-title existing-block-entity)
   (validate-disallow-page-with-journal-name new-title existing-block-entity))
 
-(defn validate-parent-property
+(defn- validate-parent-property-have-same-type
   "Validates whether given parent and children are valid. Allows 'class' and
   'page' types to have a relationship with their own type. May consider allowing more
   page types if they don't cause systemic bugs"
@@ -166,3 +143,56 @@
                      :payload {:message "Can't set this page as a parent because the child page is a different type"
                                :type :warning}
                      :blocks (map #(select-keys % [:db/id :block/title]) (remove ldb/class? child-ents))}))))
+
+(defn- disallow-built-in-class-parent-change
+  [_parent-ent child-ents]
+  (when (some #(get db-class/built-in-classes (:db/ident %)) child-ents)
+    (throw (ex-info "Can't change the parent of a built-in tag"
+                    {:type :notification
+                     :payload {:message "Can't change the parent of a built-in tag"
+                               :type :warning}}))))
+
+(defn validate-parent-property
+  [parent-ent child-ents]
+  (disallow-built-in-class-parent-change parent-ent child-ents)
+  (validate-parent-property-have-same-type parent-ent child-ents))
+
+(defn- disallow-node-cant-tag-with-built-in-non-tags
+  [db _block-eids v]
+  (let [tag-ent (d/entity db v)]
+    (when (and (:logseq.property/built-in? tag-ent)
+               (not (ldb/class? tag-ent)))
+      (throw (ex-info (str "Can't set tag with built-in page that isn't a tag " (pr-str (:block/title tag-ent)))
+                    {:type :notification
+                     :payload {:message (str "Can't set tag with built-in page that isn't a tag " (pr-str (:block/title tag-ent)))
+                               :type :error}
+                     :property-value v})))))
+
+(defn- disallow-node-cant-tag-with-private-tags
+  [db block-eids v]
+  (when (and (ldb/private-tags (:db/ident (d/entity db v)))
+             ;; Allow assets to be tagged
+             (not (and
+                   (every? (fn [id] (ldb/asset? (d/entity db id))) block-eids)
+                   (= :logseq.class/Asset (:db/ident (d/entity db v))))))
+    (throw (ex-info (str "Can't set tag with built-in #" (:block/title (d/entity db v)))
+                    {:type :notification
+                     :payload {:message (str "Can't set tag with built-in #" (:block/title (d/entity db v)))
+                               :type :error}
+                     :property-id :block/tags
+                     :property-value v}))))
+
+(defn- disallow-tagging-a-built-in-entity
+  [db block-eids]
+  (when-let [built-in-ent (some #(when (:logseq.property/built-in? %) %)
+                                (map #(d/entity db %) block-eids))]
+    (throw (ex-info (str "Can't add tag on built-in " (pr-str (:block/title built-in-ent)))
+                    {:type :notification
+                     :payload {:message (str "Can't add tag on built-in " (pr-str (:block/title built-in-ent)))
+                               :type :error}}))))
+
+(defn validate-tags-property
+  [db block-eids v]
+  (disallow-tagging-a-built-in-entity db block-eids)
+  (disallow-node-cant-tag-with-private-tags db block-eids v)
+  (disallow-node-cant-tag-with-built-in-non-tags db block-eids v))

+ 11 - 8
deps/outliner/test/logseq/outliner/pipeline_test.cljs

@@ -8,14 +8,16 @@
             [logseq.outliner.pipeline :as outliner-pipeline]
             [clojure.string :as string]
             [logseq.db.test.helper :as db-test]
-            [logseq.common.util.page-ref :as page-ref]))
+            [logseq.common.util.page-ref :as page-ref]
+            [clojure.set :as set]))
 
 (defn- get-blocks [db]
   (->> (d/q '[:find (pull ?b [* {:block/path-refs [:block/name :db/id]}])
               :in $
-              :where [?b :block/title]
-              [(missing? $ ?b :logseq.property/built-in?)]
-              [(missing? $ ?b :block/type)]]
+              :where
+              [?b :block/page]
+              [?b :block/title]
+              [(missing? $ ?b :logseq.property/built-in?)]]
             db)
        (map first)))
 
@@ -48,13 +50,14 @@
           updated-blocks (->> (get-blocks @conn)
                               ;; Only keep enough of content to uniquely identify block
                               (map #(hash-map :block/title (re-find #"\w+" (:block/title %))
-                                              :path-ref-names (set (map :block/name (:block/path-refs %))))))]
+                                              :path-ref-names (set (map :block/name (:block/path-refs %))))))
+          page-tag-refs #{"tags" "page"}]
       (is (= [{:block/title "parent"
-               :path-ref-names #{"page1" "bar"}}
+               :path-ref-names (set/union page-tag-refs #{"page1" "bar"})}
               {:block/title "child"
-               :path-ref-names #{"page1" "bar" "baz"}}
+               :path-ref-names (set/union page-tag-refs #{"page1" "bar" "baz"})}
               {:block/title "grandchild"
-               :path-ref-names #{"page1" "bar" "baz" "bing"}}]
+               :path-ref-names (set/union page-tag-refs #{"page1" "bar" "baz" "bing"})}]
              updated-blocks)))))
 
 (deftest block-content-refs

+ 3 - 2
deps/outliner/test/logseq/outliner/property_test.cljs

@@ -3,7 +3,8 @@
             [datascript.core :as d]
             [logseq.outliner.property :as outliner-property]
             [logseq.db.frontend.property :as db-property]
-            [logseq.db.test.helper :as db-test]))
+            [logseq.db.test.helper :as db-test]
+            [logseq.db :as ldb]))
 
 (deftest upsert-property!
   (testing "Creates a property"
@@ -228,7 +229,7 @@
     (testing "Add choice successfully"
       (let [_ (outliner-property/upsert-closed-value! conn :user.property/num {:value 3})
             b (first (d/q '[:find [(pull ?b [*]) ...] :where [?b :property.value/content 3]] @conn))]
-        (is (= (:block/type b) "closed value"))
+        (is (ldb/closed-value? (d/entity @conn (:db/id b))))
         (is (= [2 3]
                (map db-property/closed-value-content (:block/_closed-value-property (d/entity @conn :user.property/num)))))))
 

+ 77 - 33
deps/outliner/test/logseq/outliner/validate_test.cljs

@@ -4,70 +4,64 @@
             [logseq.outliner.validate :as outliner-validate]
             [logseq.db.test.helper :as db-test]))
 
-(defn- find-block-by-content [conn content]
-  (->> content
-       (d/q '[:find [(pull ?b [*]) ...]
-              :in $ ?content
-              :where [?b :block/title ?content] [(missing? $ ?b :logseq.property/built-in?)]]
-            @conn)
-       first))
-
 (deftest validate-block-title-unique-for-properties
   (let [conn (db-test/create-conn-with-blocks
-              ;; use a property name that's same as built-in
-              {:properties {:background-image {:block/schema {:type :default}}}})]
+              {:properties {:color {:block/schema {:type :default}}
+                            :color2 {:block/schema {:type :default}}}})]
 
     (is (nil?
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
-          "background-color"
-          (assoc (find-block-by-content conn "background-image") :db/id 10000)))
+          (:block/title (d/entity @conn :logseq.property/background-color))
+          (d/entity @conn :user.property/color)))
         "Allow user property to have same name as built-in property")
 
     (is (thrown-with-msg?
          js/Error
-         #"Duplicate property"
+         #"Duplicate page"
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
-          "background-image"
-          (assoc (find-block-by-content conn "background-image") :db/id 10000)))
+          "color"
+          (d/entity @conn :user.property/color2)))
         "Disallow duplicate user property")))
 
 (deftest validate-block-title-unique-for-pages
   (let [conn (db-test/create-conn-with-blocks
               [{:page {:block/title "page1"}}
+               {:page {:block/title "another page"}}
                {:page {:block/title "Apple" :build/tags [:Company]}}
+               {:page {:block/title "Another Company" :build/tags [:Company]}}
                {:page {:block/title "Banana" :build/tags [:Fruit]}}])]
 
     (is (thrown-with-msg?
          js/Error
-         #"Duplicate page by tag"
+         #"Duplicate page"
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
           "Apple"
-          (assoc (find-block-by-content conn "Apple") :db/id 10000)))
+          (db-test/find-page-by-title @conn "Another Company")))
         "Disallow duplicate page with tag")
     (is (nil?
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
           "Apple"
-          (find-block-by-content conn "Banana")))
+          (db-test/find-page-by-title @conn "Banana")))
         "Allow page with same name for different tag")
 
     (is (thrown-with-msg?
          js/Error
-         #"Duplicate page without tag"
+         #"Duplicate page"
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
           "page1"
-          (assoc (find-block-by-content conn "page1") :db/id 10000)))
+          (db-test/find-page-by-title @conn "another page")))
         "Disallow duplicate page without tag")
 
     (is (nil?
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
           "Apple"
-          (find-block-by-content conn "Fruit")))
+          (db-test/find-page-by-title @conn "Fruit")))
         "Allow class to have same name as a page")))
 
 (deftest validate-parent-property
@@ -77,11 +71,11 @@
                :pages-and-blocks
                [{:page {:block/title "page1"}}
                 {:page {:block/title "page2"}}]})
-        page1 (find-block-by-content conn "page1")
-        page2 (find-block-by-content conn "page2")
-        class1 (find-block-by-content conn "Class1")
-        class2 (find-block-by-content conn "Class2")
-        property (find-block-by-content conn "prop1")]
+        page1 (db-test/find-page-by-title @conn "page1")
+        page2 (db-test/find-page-by-title @conn "page2")
+        class1 (db-test/find-page-by-title @conn "Class1")
+        class2 (db-test/find-page-by-title @conn "Class2")
+        property (db-test/find-page-by-title @conn "prop1")]
 
     (testing "valid parent and child combinations"
       (is (nil? (outliner-validate/validate-parent-property page1 [page2]))
@@ -99,7 +93,52 @@
         class1 page1
         page1 class1
         property page1
-        property class1))))
+        property class1))
+
+    (testing "built-in tag can't have parent changed"
+      (is (thrown-with-msg?
+            js/Error
+            #"Can't change.*built-in"
+            (outliner-validate/validate-parent-property (d/entity @conn :logseq.class/Task)
+                                                        [(d/entity @conn :logseq.class/Cards)]))))))
+
+(deftest validate-tags-property
+  (let [conn (db-test/create-conn-with-blocks
+              {:classes {:SomeTag {}}
+               :pages-and-blocks
+               [{:page {:block/title "page1"}
+                 :blocks [{:block/title "block"}]}]})
+        block (db-test/find-block-by-content @conn "block")]
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Can't add tag.*Tag"
+         (outliner-validate/validate-tags-property @conn [:logseq.class/Tag] :user.class/SomeTag))
+        "built-in tag must not be tagged by the user")
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Can't add tag.*Heading"
+         (outliner-validate/validate-tags-property @conn [:logseq.property/heading] :user.class/SomeTag))
+        "built-in property must not be tagged by the user")
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Can't add tag.*Contents"
+         (outliner-validate/validate-tags-property @conn [(:db/id (db-test/find-page-by-title @conn "Contents"))] :user.class/SomeTag))
+        "built-in page must not be tagged by the user")
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Can't set tag.*Page"
+         (outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.class/Page))
+        "Nodes can't be tagged with built-in private tags")
+
+    (is (thrown-with-msg?
+         js/Error
+         #"Can't set tag.*Priority"
+         (outliner-validate/validate-tags-property @conn [(:db/id block)] :logseq.task/priority))
+        "Nodes can't be tagged with built-in non tags")))
 
 ;; Try as many of the validations against a new graph to confirm
 ;; that validations make sense and are valid for a new graph
@@ -107,7 +146,11 @@
   (let [conn (db-test/create-conn)]
 
     (testing "Validate pages"
-      (let [pages (d/q '[:find [(pull ?b [*]) ...] :where [?b :block/title] [?b :block/type]] @conn)
+      (let [pages (->> (d/q '[:find [?b ...] :where
+                              [?b :block/title]
+                              [?b :block/tags]] @conn)
+                       (map (fn [id]
+                              (d/entity @conn id))))
             page-errors (atom {})]
         (doseq [page pages]
           (try
@@ -123,9 +166,10 @@
             "Default pages shouldn't have any validation errors")))
 
     (testing "Validate property relationships"
-      (let [parent-child-pairs (d/q '[:find (pull ?parent [:block/title :block/type])
-                                      (pull ?child [:block/title :block/type])
+      (let [parent-child-pairs (d/q '[:find ?parent ?child
                                       :where [?child :logseq.property/parent ?parent]] @conn)]
-        (doseq [[parent child] parent-child-pairs]
-          (is (nil? (outliner-validate/validate-parent-property parent [child]))
-              (str "Parent and child page is valid: " (pr-str (:block/title parent)) " " (pr-str (:block/title child)))))))))
+        (doseq [[parent-id child-id] parent-child-pairs]
+          (let [parent (d/entity @conn parent-id)
+                child (d/entity @conn child-id)]
+            (is (nil? (#'outliner-validate/validate-parent-property-have-same-type parent [child]))
+                (str "Parent and child page is valid: " (pr-str (:block/title parent)) " " (pr-str (:block/title child))))))))))

+ 450 - 437
deps/shui/src/logseq/shui/demo.cljs

@@ -16,475 +16,488 @@
   []
   (let [icon #(ui/tabler-icon (name %1) {:class "scale-90 pr-1 opacity-80"})]
     (ui/dropdown-menu-content
-      {:class "w-56"
-       :on-click (fn [^js e] (some-> (.-target e) (.-innerText)
-                               (#(identity ["You select: " [:b.text-red-700 %1]])) (ui/toast! :info)))}
-      (ui/dropdown-menu-label "My Account")
-      (ui/dropdown-menu-separator)
-      (ui/dropdown-menu-group
+     {:class "w-56"
+      :on-click (fn [^js e] (some-> (.-target e) (.-innerText)
+                                    (#(identity ["You select: " [:b.text-red-700 %1]])) (ui/toast! :info)))}
+     (ui/dropdown-menu-label "My Account")
+     (ui/dropdown-menu-separator)
+     (ui/dropdown-menu-group
         ;; items
-        (ui/dropdown-menu-item (icon :user) "Profile" (ui/dropdown-menu-shortcut "⌘P"))
-        (ui/dropdown-menu-item (icon :brand-mastercard) [:span "Billing"] (ui/dropdown-menu-shortcut "⌘B"))
-        (ui/dropdown-menu-item (icon :adjustments-alt) [:span "Settings"] (ui/dropdown-menu-shortcut "⌘,"))
-        (ui/dropdown-menu-item (icon :keyboard) [:span "Keyboard shortcuts"]))
-      (ui/dropdown-menu-separator)
+      (ui/dropdown-menu-item (icon :user) "Profile" (ui/dropdown-menu-shortcut "⌘P"))
+      (ui/dropdown-menu-item (icon :brand-mastercard) [:span "Billing"] (ui/dropdown-menu-shortcut "⌘B"))
+      (ui/dropdown-menu-item (icon :adjustments-alt) [:span "Settings"] (ui/dropdown-menu-shortcut "⌘,"))
+      (ui/dropdown-menu-item (icon :keyboard) [:span "Keyboard shortcuts"]))
+     (ui/dropdown-menu-separator)
       ;; group
-      (ui/dropdown-menu-group
+     (ui/dropdown-menu-group
         ;; items
-        (ui/dropdown-menu-item (icon :users) "Team")
+      (ui/dropdown-menu-item (icon :users) "Team")
         ;; sub menu
-        (ui/dropdown-menu-sub
-          (ui/dropdown-menu-sub-trigger
-            (icon :user-plus) [:span "Invite users"])
-          (ui/dropdown-menu-sub-content
-            (ui/dropdown-menu-item (icon :mail) "Email")
-            (ui/dropdown-menu-item (icon :message) "Message")
-            (ui/dropdown-menu-item (icon :dots-circle-horizontal) "More...")))
+      (ui/dropdown-menu-sub
+       (ui/dropdown-menu-sub-trigger
+        (icon :user-plus) [:span "Invite users"])
+       (ui/dropdown-menu-sub-content
+        (ui/dropdown-menu-item (icon :mail) "Email")
+        (ui/dropdown-menu-item (icon :message) "Message")
+        (ui/dropdown-menu-item (icon :dots-circle-horizontal) "More...")))
         ;; menu item
-        (ui/dropdown-menu-item (icon :plus) "New Team" (ui/dropdown-menu-shortcut "⌘+T")))
-      (ui/dropdown-menu-separator)
-      (ui/dropdown-menu-item (icon :brand-github) "GitHub")
-      (ui/dropdown-menu-item {:disabled true} (icon :cloud) "Cloud API")
-      (ui/dropdown-menu-separator)
-      (ui/dropdown-menu-item (icon :logout) "Logout" (ui/dropdown-menu-shortcut "⌘+Q"))
-      )))
+      (ui/dropdown-menu-item (icon :plus) "New Team" (ui/dropdown-menu-shortcut "⌘+T")))
+     (ui/dropdown-menu-separator)
+     (ui/dropdown-menu-item (icon :brand-github) "GitHub")
+     (ui/dropdown-menu-item {:disabled true} (icon :cloud) "Cloud API")
+     (ui/dropdown-menu-separator)
+     (ui/dropdown-menu-item (icon :logout) "Logout" (ui/dropdown-menu-shortcut "⌘+Q")))))
 
 (rum/defc sample-context-menu-content
   []
   (let [icon #(ui/tabler-icon (name %1) {:class "scale-90 pr-1 opacity-80"})]
     (ui/context-menu
       ;; trigger
-      (ui/context-menu-trigger
-        [:div.border.px-6.py-12.border-dashed.rounded.text-center.select-none
-         {:key "ctx-menu-click"}
-         [:span.opacity-50 "Right click here"]])
+     (ui/context-menu-trigger
+      [:div.border.px-6.py-12.border-dashed.rounded.text-center.select-none
+       {:key "ctx-menu-click"}
+       [:span.opacity-50 "Right click here"]])
       ;; content
-      (ui/context-menu-content
-        {:class "w-60 max-h-[80vh] overflow-auto"}
-        (ui/context-menu-item
-          (icon "arrow-left")
-          "Back"
-          (ui/context-menu-shortcut "⌘["))
-        (ui/context-menu-item {:disabled true}
-          (icon "arrow-right")
-          "Forward"
-          (ui/context-menu-shortcut "⌘]"))
-        (ui/context-menu-item
-          (icon "refresh")
-          "Reload"
-          (ui/context-menu-shortcut "⌘R"))
+     (ui/context-menu-content
+      {:class "w-60 max-h-[80vh] overflow-auto"}
+      (ui/context-menu-item
+       (icon "arrow-left")
+       "Back"
+       (ui/context-menu-shortcut "⌘["))
+      (ui/context-menu-item {:disabled true}
+                            (icon "arrow-right")
+                            "Forward"
+                            (ui/context-menu-shortcut "⌘]"))
+      (ui/context-menu-item
+       (icon "refresh")
+       "Reload"
+       (ui/context-menu-shortcut "⌘R"))
         ;; Sub menu
-        (ui/context-menu-sub
-          (ui/context-menu-sub-trigger {:inset true} "More tools")
-          (ui/context-menu-sub-content {:class "w-48"}
-            (ui/context-menu-item "Save page As..."
-              (ui/context-menu-shortcut "⇧⌘S"))
-            (ui/context-menu-item "Create Shortcut...")
-            (ui/context-menu-item "Name Window...")
-            (ui/context-menu-separator)
-            (ui/context-menu-item "Developer Tools")))
+      (ui/context-menu-sub
+       (ui/context-menu-sub-trigger {:inset true} "More tools")
+       (ui/context-menu-sub-content {:class "w-48"}
+                                    (ui/context-menu-item "Save page As..."
+                                                          (ui/context-menu-shortcut "⇧⌘S"))
+                                    (ui/context-menu-item "Create Shortcut...")
+                                    (ui/context-menu-item "Name Window...")
+                                    (ui/context-menu-separator)
+                                    (ui/context-menu-item "Developer Tools")))
         ;; more
-        (ui/context-menu-separator)
-        (ui/context-menu-checkbox-item {:checked true}
-          "Show Bookmarks Bar" (ui/context-menu-shortcut "⌘⇧B"))
-        (ui/context-menu-checkbox-item "Show Full URLs")
-        (ui/context-menu-separator)
-        (ui/context-menu-radio-group {:value "pedro"}
-          (ui/context-menu-label {:inset true} "People")
-          (ui/context-menu-separator)
-          (ui/context-menu-radio-item {:value "pedro"} "Pedro Duarte")
-          (ui/context-menu-radio-item {:value "colm"} "Colm Tuite"))))))
+      (ui/context-menu-separator)
+      (ui/context-menu-checkbox-item {:checked true}
+                                     "Show Bookmarks Bar" (ui/context-menu-shortcut "⌘⇧B"))
+      (ui/context-menu-checkbox-item "Show Full URLs")
+      (ui/context-menu-separator)
+      (ui/context-menu-radio-group {:value "pedro"}
+                                   (ui/context-menu-label {:inset true} "People")
+                                   (ui/context-menu-separator)
+                                   (ui/context-menu-radio-item {:value "pedro"} "Pedro Duarte")
+                                   (ui/context-menu-radio-item {:value "colm"} "Colm Tuite"))))))
+
+(rum/defc sample-tabs
+  []
+  (ui/tabs
+   {:defaultValue "account"
+    :className "w-[400px]"}
+   (ui/tabs-list
+    (ui/tabs-trigger
+     {:value "account"}
+     "Account")
+    (ui/tabs-trigger
+     {:value "password"}
+     "Password"))
+   (ui/tabs-content
+    {:value "account"}
+    "Make changes to your account here.")
+   (ui/tabs-content
+    {:value "password"}
+    "Change your password here.")))
 
 (rum/defc sample-form-basic
   []
   [:div.border.p-6.rounded.bg-gray-01
    (let [form-ctx (form-core/use-form
-                    {:defaultValues {:username ""
-                                     :agreement true
-                                     :notification "all"
-                                     :bio ""}
-                     :yupSchema (-> (.object yup)
-                                  (.shape #js {:username (-> (.string yup) (.required))})
-                                  (.required))})
+                   {:defaultValues {:username ""
+                                    :agreement true
+                                    :notification "all"
+                                    :bio ""}
+                    :yupSchema (-> (.object yup)
+                                   (.shape #js {:username (-> (.string yup) (.required))})
+                                   (.required))})
          handle-submit (:handleSubmit form-ctx)
          on-submit-valid (handle-submit
-                           (fn [^js e]
-                             (js/console.log "[form] submit: " e)
-                             (js/alert (js/JSON.stringify e nil 2))))]
+                          (fn [^js e]
+                            (js/console.log "[form] submit: " e)
+                            (js/alert (js/JSON.stringify e nil 2))))]
 
      (ui/form-provider form-ctx
-       [:form
-        {:on-submit on-submit-valid}
+                       [:form
+                        {:on-submit on-submit-valid}
 
         ;; field item
-        (ui/form-field {:name "username"}
-          (fn [field error]
-            (ui/form-item
-              (ui/form-label "Username")
-              (ui/form-control
-                (ui/input (merge {:placeholder "Username"} field)))
-              (ui/form-description
-                (if error
-                  [:b.text-red-800 (:message error)]
-                  "This is your public display name.")))))
-
-        (ui/form-field {:name "bio"}
-          (fn [field error]
-            (ui/form-item
-              {:class "pt-4"}
-              (ui/form-control
-                (ui/textarea (merge {:placeholder "Bio text..."} field))))))
+                        (ui/form-field {:name "username"}
+                                       (fn [field error]
+                                         (ui/form-item
+                                          (ui/form-label "Username")
+                                          (ui/form-control
+                                           (ui/input (merge {:placeholder "Username"} field)))
+                                          (ui/form-description
+                                           (if error
+                                             [:b.text-red-800 (:message error)]
+                                             "This is your public display name.")))))
+
+                        (ui/form-field {:name "bio"}
+                                       (fn [field error]
+                                         (ui/form-item
+                                          {:class "pt-4"}
+                                          (ui/form-control
+                                           (ui/textarea (merge {:placeholder "Bio text..."} field))))))
 
         ;; radio
-        (ui/form-field {:name "notification"}
+                        (ui/form-field {:name "notification"}
           ;; item render
-          (fn [field]
-            (ui/form-item
-              {:class "space-y-3 my-4"}
-              (ui/form-label "Notify me about...")
-              (ui/form-control
-                (ui/radio-group
-                  {:value (:value field)
-                   :on-value-change (:onChange field)
-                   :class "flex flex-col space-y-3"}
-                  (ui/form-item
-                    {:class "flex flex-row space-x-3 items-center space-y-0"}
-                    (ui/form-control
-                      (ui/radio-group-item {:value "all"}))
-                    (ui/form-label "All"))
-
-                  (ui/form-item
-                    {:class "flex flex-row space-x-3 items-center space-y-0"}
-                    (ui/form-control
-                      (ui/radio-group-item {:value "direct"}))
-                    (ui/form-label "Direct messages and mentions")))))))
-
-        [:hr]
+                                       (fn [field]
+                                         (ui/form-item
+                                          {:class "space-y-3 my-4"}
+                                          (ui/form-label "Notify me about...")
+                                          (ui/form-control
+                                           (ui/radio-group
+                                            {:value (:value field)
+                                             :on-value-change (:onChange field)
+                                             :class "flex flex-col space-y-3"}
+                                            (ui/form-item
+                                             {:class "flex flex-row space-x-3 items-center space-y-0"}
+                                             (ui/form-control
+                                              (ui/radio-group-item {:value "all"}))
+                                             (ui/form-label "All"))
+
+                                            (ui/form-item
+                                             {:class "flex flex-row space-x-3 items-center space-y-0"}
+                                             (ui/form-control
+                                              (ui/radio-group-item {:value "direct"}))
+                                             (ui/form-label "Direct messages and mentions")))))))
+
+                        [:hr]
 
         ;; checkbox
-        (ui/form-field {:name "agreement"}
-          (fn [field]
-            (ui/form-item
-              {:class "flex justify-start items-center space-x-3 space-y-0 my-3 pr-3"}
-              (ui/form-control
-                (ui/checkbox {:checked (:value field)
-                              :on-checked-change (:onChange field)}))
-              (ui/form-label {:class "font-normal cursor-pointer"} "Agreement terms"))))
+                        (ui/form-field {:name "agreement"}
+                                       (fn [field]
+                                         (ui/form-item
+                                          {:class "flex justify-start items-center space-x-3 space-y-0 my-3 pr-3"}
+                                          (ui/form-control
+                                           (ui/checkbox {:checked (:value field)
+                                                         :on-checked-change (:onChange field)}))
+                                          (ui/form-label {:class "font-normal cursor-pointer"} "Agreement terms"))))
 
         ;; actions
-        [:div.relative.px-2
-         (ui/button {:type "submit" :class "!absolute right-0 top-[-40px]"} "Submit")]]))])
+                        [:div.relative.px-2
+                         (ui/button {:type "submit" :class "!absolute right-0 top-[-40px]"} "Submit")]]))])
 
 (rum/defc sample-date-picker
   []
   (let [[open? set-open!] (rum/use-state false)
         [date set-date!] (rum/use-state (js/Date.))]
     (ui/popover
-      {:open open?
-       :on-open-change (fn [o] (set-open! o))}
+     {:open open?
+      :on-open-change (fn [o] (set-open! o))}
       ;; trigger
-      (ui/popover-trigger
-        {:as-child true
-         :class "w-2/3"}
-        (ui/input
-          {:type :text
-           :placeholder "pick a date"
-           :default-value (.toDateString date)}))
+     (ui/popover-trigger
+      {:as-child true
+       :class "w-2/3"}
+      (ui/input
+       {:type :text
+        :placeholder "pick a date"
+        :default-value (.toDateString date)}))
       ;; content
-      (ui/popover-content
-        {:on-open-auto-focus #(.preventDefault %)
-         :side-offset 8
-         :class "p-0"}
-        (ui/calendar
-          {:selected date
-           :on-day-click
-           (fn [^js d]
-             (set-date! d)
-             (set-open! false))})))))
+     (ui/popover-content
+      {:on-open-auto-focus #(.preventDefault %)
+       :side-offset 8
+       :class "p-0"}
+      (ui/calendar
+       {:selected date
+        :on-day-click
+        (fn [^js d]
+          (set-date! d)
+          (set-open! false))})))))
 
 (rum/defc sample-dialog-basic
   []
   (let [[open? set-open!] (rum/use-state false)]
     (ui/dialog
-      {:open open?
-       :on-open-change #(set-open! %)}
-      (ui/dialog-trigger
-        {:as-child true}
-        (ui/button {:variant :outline}
-          (ui/tabler-icon "notification") "Open as modal locally"))
-      (ui/dialog-content
-        (ui/dialog-header
-          (ui/dialog-title "Header")
-          (ui/dialog-description
-            "Description"))
-        [:div.max-h-96.overflow-y-auto
-         {:class "-mx-6"}
-         [:section.px-6
-          (repeat 8 [:p "Your custom content"])]]
-        (ui/dialog-footer
-          (ui/button
-            {:on-click #(set-open! false)
-             :size :md} "🍄 * Footer"))))))
-
+     {:open open?
+      :on-open-change #(set-open! %)}
+     (ui/dialog-trigger
+      {:as-child true}
+      (ui/button {:variant :outline}
+                 (ui/tabler-icon "notification") "Open as modal locally"))
+     (ui/dialog-content
+      (ui/dialog-header
+       (ui/dialog-title "Header")
+       (ui/dialog-description
+        "Description"))
+      [:div.max-h-96.overflow-y-auto
+       {:class "-mx-6"}
+       [:section.px-6
+        (repeat 8 [:p "Your custom content"])]]
+      (ui/dialog-footer
+       (ui/button
+        {:on-click #(set-open! false)
+         :size :md} "🍄 * Footer"))))))
 
 (rum/defc page []
   (ui/tooltip-provider
-    [:div.sm:p-10
-     [:hr]
-     [:input
-      {:type "checkbox" :on-change #(js/console.log "===>> onChange:" % (.-value (.-target %)))}]
-     (ui/checkbox {:on-click
-                   (fn [^js e] (js/console.log "==>> click:"
-                                 (set! (. (.-target e) -checked) (.-state (.-dataset (.-target e))))
-                                 (.-checked (.-target e))
-                                 ))
-                   :on-checked-change #(js/console.log "==>> on checked change:" %)
-                   } "abc")
-
-     [:h1.text-3xl.font-bold "Logseq UI"]
-     [:hr]
+   [:div.sm:p-10
+    [:hr]
+    [:input
+     {:type "checkbox" :on-change #(js/console.log "===>> onChange:" % (.-value (.-target %)))}]
+    (ui/checkbox {:on-click
+                  (fn [^js e] (js/console.log "==>> click:"
+                                              (set! (. (.-target e) -checked) (.-state (.-dataset (.-target e))))
+                                              (.-checked (.-target e))))
+                  :on-checked-change #(js/console.log "==>> on checked change:" %)} "abc")
+
+    [:h1.text-3xl.font-bold "Logseq UI"]
+    [:hr]
 
      ;; Button
-     (section-item "Button"
-       [:div.flex.flex-row.flex-wrap.gap-2
-        (let [[loading? set-loading!] (rum/use-state false)]
-          (ui/button
-            {:size :sm
-             :on-click (fn []
-                         (set-loading! true)
-                         (js/setTimeout #(set-loading! false) 5000))
-             :disabled loading?}
-            (when loading?
-              (ui/tabler-icon "loader2" {:class "animate-spin"}))
-            "Logseq Classic Button"
-            (ui/tabler-icon "arrow-right")))
-
-        (ui/button {:variant :outline :size :sm} "Outline")
-        (ui/button {:variant :secondary :size :sm} "Secondary")
-        (ui/button {:disabled true :size :sm} "Disabled")
-        (ui/button {:variant :destructive :size :sm} "Destructive")
-        (ui/button {:class "primary-green" :size :sm} "Custom (.primary-green)")
-        (ui/button {:variant :ghost :size :sm} "Ghost")
-        (ui/button {:variant :link :size :sm} "Link")
-        (ui/button
-          {:variant :icon
-           :size :sm}
-          [:a.flex.items-center.text-blue-rx-10.hover:text-blue-rx-10-alpha
-           {:href "https://x.com/logseq" :target "_blank"}
-           (ui/tabler-icon "brand-twitter" {:size 15})]
-          )])
-
-     ;; Toast
-     (section-item "Toast"
-       [:div.flex.flex-row.flex-wrap.gap-2
-        (ui/button
-          {:size :md
-           :variant :outline
-           :on-click #(ui/toast!
-                        "Check for updates ..."
-                        (nth [:success :error :default :info :warning] (rand-int 3))
-                        {:title (if (odd? (js/Date.now)) "History of China" "")
-                         :duration 3000})}
-          "Open random toast"
-          (ui/tabler-icon "arrow-right"))
-
-        (ui/button
-          {:variant :secondary
-           :size :md
-           :on-click (fn []
-                       (ui/toast!
-                         (fn [{:keys [id dismiss! update!]}]
-                           [:b.text-red-700
-                            [:div.flex.items-center.gap-2
-                             (ui/tabler-icon "info-circle")
-                             (str "#(" id ") ")
-                             (.toLocaleString (js/Date.))]
-                            [:div.flex.flex-row.gap-2
-                             (ui/button
-                               {:on-click #(dismiss! id) :size :sm}
-                               "x close")
-
-                             (ui/button
-                               {:on-click #(update! {:title (js/Date.now)
-                                                     :action [:b (ui/button {:on-click (fn [] (ui/toast-dismiss!))} "clear all")]})
-                                :size :sm}
-                               "x update")]])
-                         :default
-                         {:duration 3000 :onDismiss #(js/console.log "===>> dismiss?:" %1)}))}
-          (ui/tabler-icon "apps")
-          "Toast callback handle")
-
-        (ui/button
-          {:on-click #(ui/toast! "A message from SoundCloud..."
-                        {:class "text-orange-rx-10"
-                         :icon [:b.pl-1 (ui/tabler-icon "brand-soundcloud" {:size 20})]
-                         :duration 3000})
-           :class "primary-orange"
-           :size :md}
-          "Custom icon")])
-
-     [:div.flex.flex-row.space-x-16.items-center
+    (section-item "Button"
+                  [:div.flex.flex-row.flex-wrap.gap-2
+                   (let [[loading? set-loading!] (rum/use-state false)]
+                     (ui/button
+                      {:size :sm
+                       :on-click (fn []
+                                   (set-loading! true)
+                                   (js/setTimeout #(set-loading! false) 5000))
+                       :disabled loading?}
+                      (when loading?
+                        (ui/tabler-icon "loader2" {:class "animate-spin"}))
+                      "Logseq Classic Button"
+                      (ui/tabler-icon "arrow-right")))
+
+                   (ui/button {:variant :outline :size :sm} "Outline")
+                   (ui/button {:variant :secondary :size :sm} "Secondary")
+                   (ui/button {:disabled true :size :sm} "Disabled")
+                   (ui/button {:variant :destructive :size :sm} "Destructive")
+                   (ui/button {:class "primary-green" :size :sm} "Custom (.primary-green)")
+                   (ui/button {:variant :ghost :size :sm} "Ghost")
+                   (ui/button {:variant :link :size :sm} "Link")
+                   (ui/button
+                    {:variant :icon
+                     :size :sm}
+                    [:a.flex.items-center.text-blue-rx-10.hover:text-blue-rx-10-alpha
+                     {:href "https://x.com/logseq" :target "_blank"}
+                     (ui/tabler-icon "brand-twitter" {:size 15})])])
+
+;; Toast
+    (section-item "Toast"
+                  [:div.flex.flex-row.flex-wrap.gap-2
+                   (ui/button
+                    {:size :md
+                     :variant :outline
+                     :on-click #(ui/toast!
+                                 "Check for updates ..."
+                                 (nth [:success :error :default :info :warning] (rand-int 3))
+                                 {:title (if (odd? (js/Date.now)) "History of China" "")
+                                  :duration 3000})}
+                    "Open random toast"
+                    (ui/tabler-icon "arrow-right"))
+
+                   (ui/button
+                    {:variant :secondary
+                     :size :md
+                     :on-click (fn []
+                                 (ui/toast!
+                                  (fn [{:keys [id dismiss! update!]}]
+                                    [:b.text-red-700
+                                     [:div.flex.items-center.gap-2
+                                      (ui/tabler-icon "info-circle")
+                                      (str "#(" id ") ")
+                                      (.toLocaleString (js/Date.))]
+                                     [:div.flex.flex-row.gap-2
+                                      (ui/button
+                                       {:on-click #(dismiss! id) :size :sm}
+                                       "x close")
+
+                                      (ui/button
+                                       {:on-click #(update! {:title (js/Date.now)
+                                                             :action [:b (ui/button {:on-click (fn [] (ui/toast-dismiss!))} "clear all")]})
+                                        :size :sm}
+                                       "x update")]])
+                                  :default
+                                  {:duration 3000 :onDismiss #(js/console.log "===>> dismiss?:" %1)}))}
+                    (ui/tabler-icon "apps")
+                    "Toast callback handle")
+
+                   (ui/button
+                    {:on-click #(ui/toast! "A message from SoundCloud..."
+                                           {:class "text-orange-rx-10"
+                                            :icon [:b.pl-1 (ui/tabler-icon "brand-soundcloud" {:size 20})]
+                                            :duration 3000})
+                     :class "primary-orange"
+                     :size :md}
+                    "Custom icon")])
+
+    [:div.flex.flex-row.space-x-16.items-center
       ;; Tips
-      (section-item "Tips"
-        [:div.flex.flex-row.flex-wrap.gap-2
-         (ui/tooltip-provider
-           (ui/tooltip
-             (ui/tooltip-trigger
-               (ui/button
-                 {:variant :outline
-                  :on-click #(dialog-core/open! [:h1.text-9xl.text-center.scale-110 "🍄"])}
-                 "Tip for hint?"))
-             (ui/tooltip-content
-               {:class "w-42 px-8 py-4 text-xl border-green-rx-08 bg-green-rx-07-alpha"}
-               "🍄")))])
+     (section-item "Tips"
+                   [:div.flex.flex-row.flex-wrap.gap-2
+                    (ui/tooltip-provider
+                     (ui/tooltip
+                      (ui/tooltip-trigger
+                       (ui/button
+                        {:variant :outline
+                         :on-click #(dialog-core/open! [:h1.text-9xl.text-center.scale-110 "🍄"])}
+                        "Tip for hint?"))
+                      (ui/tooltip-content
+                       {:class "w-42 px-8 py-4 text-xl border-green-rx-08 bg-green-rx-07-alpha"}
+                       "🍄")))])
       ;; Avatar
-      (section-item "Avatar"
-        [:div.flex.flex-row.space-x-6.items-center
-         (ui/avatar
-           (ui/avatar-image {:src "https://avatars.githubusercontent.com/u/63385289?s=200&v=4"})
-           (ui/avatar-fallback "L"))
-         (ui/avatar
-           (ui/avatar-fallback "CH"))])]
+     (section-item "Avatar"
+                   [:div.flex.flex-row.space-x-6.items-center
+                    (ui/avatar
+                     (ui/avatar-image {:src "https://avatars.githubusercontent.com/u/63385289?s=200&v=4"})
+                     (ui/avatar-fallback "L"))
+                    (ui/avatar
+                     (ui/avatar-fallback "CH"))])]
 
      ;; Badge
-     (section-item "Badge"
-       [:div.flex.flex-row.flex-wrap.gap-2
-        (ui/badge "Default")
-        (ui/badge {:variant :outline} "Outline")
-        (ui/badge {:variant :secondary} "Secondary")
-        (ui/badge {:variant :destructive} "Destructive")
-        (ui/badge {:class "primary-yellow"} "Custom (.primary-yellow)")])
-
-     [:div.grid.sm:grid-cols-3.sm:gap-8
+    (section-item "Badge"
+                  [:div.flex.flex-row.flex-wrap.gap-2
+                   (ui/badge "Default")
+                   (ui/badge {:variant :outline} "Outline")
+                   (ui/badge {:variant :secondary} "Secondary")
+                   (ui/badge {:variant :destructive} "Destructive")
+                   (ui/badge {:class "primary-yellow"} "Custom (.primary-yellow)")])
+
+    [:div.grid.sm:grid-cols-3.sm:gap-8
       ;; Dropdown
-      (section-item "Dropdown"
-        (ui/dropdown-menu
-          (ui/tooltip
-            (ui/tooltip-trigger
-              (ui/dropdown-menu-trigger
-                {:as-child true}
-                (ui/button {:variant :outline}
-                  (ui/tabler-icon "list") "Open dropdown menu")))
-            (ui/tooltip-content "test hide?"))
-
-          (sample-dropdown-menu-content)))
+     (section-item "Dropdown"
+                   (ui/dropdown-menu
+                    (ui/tooltip
+                     (ui/tooltip-trigger
+                      (ui/dropdown-menu-trigger
+                       {:as-child true}
+                       (ui/button {:variant :outline}
+                                  (ui/tabler-icon "list") "Open dropdown menu")))
+                     (ui/tooltip-content "test hide?"))
+
+                    (sample-dropdown-menu-content)))
 
       ;; Context menu
-      [:div.col-span-2
-       (section-item "Context Menu"
-         (sample-context-menu-content))]]
+     [:div.col-span-2
+      (section-item "Context Menu"
+                    (sample-context-menu-content))]]
+
+    (section-item "Tabs" (sample-tabs))
 
      ;; Dialog
-     (section-item "Dialog"
-       [:div.flex.flex-row.flex-wrap.gap-2
-        (sample-dialog-basic)
-        (ui/button
-          {:on-click #(dialog-core/open! "a modal dialog from `open!`" {:title "Title"})}
-          "Imperative API: open!")
-
-        (ui/button
-          {:class "primary-yellow"
-           :on-click (fn []
-                       (-> (dialog-core/alert!
-                             "a alert dialog from `alert!`"
-                             {:title [:div.flex.flex-row.space-x-2.items-center
-                                      (ui/tabler-icon "alert-triangle" {:size 18})
-                                      [:span "Alert"]]})
-                         (p/then #(js/console.log "=> alert (promise): " %))))}
-          "Imperative API: alert!")
-
-        (ui/button
-          {:class "primary-green"
-           :on-click (fn []
-                       (-> (dialog-core/confirm!
-                             "a alert dialog from `confirm!`"
-                             {:title [:div.flex.flex-row.space-x-2.items-center
-                                      (ui/tabler-icon "alert-triangle" {:size 18})
-                                      [:span "Confirm"]]})
-                         (p/then #(js/console.log "=> confirm (promise): " %))
-                         (p/catch #(js/console.log "=> confirm (promise): " %))))}
-          "Imperative API: confirm!")])
+    (section-item "Dialog"
+                  [:div.flex.flex-row.flex-wrap.gap-2
+                   (sample-dialog-basic)
+                   (ui/button
+                    {:on-click #(dialog-core/open! "a modal dialog from `open!`" {:title "Title"})}
+                    "Imperative API: open!")
+
+                   (ui/button
+                    {:class "primary-yellow"
+                     :on-click (fn []
+                                 (-> (dialog-core/alert!
+                                      "a alert dialog from `alert!`"
+                                      {:title [:div.flex.flex-row.space-x-2.items-center
+                                               (ui/tabler-icon "alert-triangle" {:size 18})
+                                               [:span "Alert"]]})
+                                     (p/then #(js/console.log "=> alert (promise): " %))))}
+                    "Imperative API: alert!")
+
+                   (ui/button
+                    {:class "primary-green"
+                     :on-click (fn []
+                                 (-> (dialog-core/confirm!
+                                      "a alert dialog from `confirm!`"
+                                      {:title [:div.flex.flex-row.space-x-2.items-center
+                                               (ui/tabler-icon "alert-triangle" {:size 18})
+                                               [:span "Confirm"]]})
+                                     (p/then #(js/console.log "=> confirm (promise): " %))
+                                     (p/catch #(js/console.log "=> confirm (promise): " %))))}
+                    "Imperative API: confirm!")])
 
      ;; Alert
-     (section-item "Alert"
-       [:<>
-        (ui/alert
-          {:class "text-orange-rx-09 border-orange-rx-07-alpha mb-4"}
-          (ui/tabler-icon "brand-soundcloud")
-          (ui/alert-title "Title is SoundCloud")
-          (ui/alert-description
-            "content: radix colors for Logseq"))
-        (ui/alert
-          (ui/tabler-icon "brand-github")
-          (ui/alert-title "GitHub")
-          (ui/alert-description
-            "content: radix colors for Logseq"))])
+    (section-item "Alert"
+                  [:<>
+                   (ui/alert
+                    {:class "text-orange-rx-09 border-orange-rx-07-alpha mb-4"}
+                    (ui/tabler-icon "brand-soundcloud")
+                    (ui/alert-title "Title is SoundCloud")
+                    (ui/alert-description
+                     "content: radix colors for Logseq"))
+                   (ui/alert
+                    (ui/tabler-icon "brand-github")
+                    (ui/alert-title "GitHub")
+                    (ui/alert-description
+                     "content: radix colors for Logseq"))])
 
      ;; Slider
-     [:div.grid.sm:grid-cols-8.gap-4
-      [:div.col-span-4.mr-6
-       (section-item "Slider" (ui/slider))]
-      [:div.col-span-1
-       (section-item "Switch"
-         (ui/switch {:size :sm :class "relative top-[-8px]"}))]
-      [:div.col-span-3.pl-4.pr-2
-       (section-item "Select"
-         (ui/select
-           {:on-value-change (fn [v] (ui/toast! v :info))}
+    [:div.grid.sm:grid-cols-8.gap-4
+     [:div.col-span-4.mr-6
+      (section-item "Slider" (ui/slider))]
+     [:div.col-span-1
+      (section-item "Switch"
+                    (ui/switch {:size :sm :class "relative top-[-8px]"}))]
+     [:div.col-span-3.pl-4.pr-2
+      (section-item "Select"
+                    (ui/select
+                     {:on-value-change (fn [v] (ui/toast! v :info))}
            ;; trigger
-           (ui/select-trigger
-             (ui/select-value {:placeholder "Select a fruit"}))
+                     (ui/select-trigger
+                      (ui/select-value {:placeholder "Select a fruit"}))
            ;; content
-           (ui/select-content
-             (ui/select-group
-               (ui/select-label "Fruits")
-               (ui/select-item {:value "apple"} "Apple")
-               (ui/select-item {:value "pear"} "Pear")
-               (ui/select-item {:value "grapes"} "Grapes")
-
-               ))))]]
-
-     ;; Form
-     (section-item "Form"
-       [:<>
-        (sample-form-basic)])
+                     (ui/select-content
+                      (ui/select-group
+                       (ui/select-label "Fruits")
+                       (ui/select-item {:value "apple"} "Apple")
+                       (ui/select-item {:value "pear"} "Pear")
+                       (ui/select-item {:value "grapes"} "Grapes")))))]]
+
+;; Form
+    (section-item "Form"
+                  [:<>
+                   (sample-form-basic)])
 
      ;; Card
-     [:div.grid.sm:grid-cols-2.sm:gap-8
-      (section-item "Card"
-        (ui/card
-          (ui/card-header
-            (ui/card-title "Title")
-            (ui/card-description "Description"))
-          (ui/card-content "This is content")
-          (ui/card-footer "Footer")))
-
-      (section-item "Skeleton"
-        (ui/card
-          (ui/card-header
-            (ui/card-title
-              (ui/skeleton {:class "h-4 w-1/2"}))
-            (ui/card-description
-              (ui/skeleton {:class "h-2 w-full"})))
-          (ui/card-content
-            (ui/skeleton {:class "h-3 mb-1"})
-            (ui/skeleton {:class "h-3 mb-1"})
-            (ui/skeleton {:class "h-3 w-2/3"}))
-
-          (ui/card-footer
-            (ui/skeleton {:class "h-4 w-full mb-2"}))))]
+    [:div.grid.sm:grid-cols-2.sm:gap-8
+     (section-item "Card"
+                   (ui/card
+                    (ui/card-header
+                     (ui/card-title "Title")
+                     (ui/card-description "Description"))
+                    (ui/card-content "This is content")
+                    (ui/card-footer "Footer")))
+
+     (section-item "Skeleton"
+                   (ui/card
+                    (ui/card-header
+                     (ui/card-title
+                      (ui/skeleton {:class "h-4 w-1/2"}))
+                     (ui/card-description
+                      (ui/skeleton {:class "h-2 w-full"})))
+                    (ui/card-content
+                     (ui/skeleton {:class "h-3 mb-1"})
+                     (ui/skeleton {:class "h-3 mb-1"})
+                     (ui/skeleton {:class "h-3 w-2/3"}))
+
+                    (ui/card-footer
+                     (ui/skeleton {:class "h-4 w-full mb-2"}))))]
 
      ;; Calendar
-     [:div.grid.sm:grid-cols-2.sm:gap-8
-      (section-item "Calendar"
-        (ui/card
-          {:class "inline-flex"}
-          (ui/calendar {:on-day-click #(ui/toast! (.toString %) :success)})))
-      (section-item "Date Picker"
-        (sample-date-picker))]
-
-     [:hr.mb-80]]))
+    [:div.grid.sm:grid-cols-2.sm:gap-8
+     (section-item "Calendar"
+                   (ui/card
+                    {:class "inline-flex"}
+                    (ui/calendar {:on-day-click #(ui/toast! (.toString %) :success)})))
+     (section-item "Date Picker"
+                   (sample-date-picker))]
 
+    [:hr.mb-80]]))
 
 (defn- get-head-container
   []
@@ -499,44 +512,44 @@
 
   (let [el-ref (rum/use-ref nil)]
     (rum/use-effect!
-      (fn []
-        (let [^js container (get-main-scroll-container)
-              ^js el (rum/deref el-ref)
-              ^js cls (.-classList el)
-              *ticking? (volatile! false)
-              el-top (-> el (.getBoundingClientRect) (.-top))
-              head-top (-> (get-head-container) (js/getComputedStyle) (.-height) (js/parseInt))
-              translate (fn [offset]
-                          (set! (. (.-style el) -transform) (str "translate3d(0, " offset "px , 0)"))
-                          (if (zero? offset)
-                            (.remove cls "translated")
-                            (.add cls "translated")))
-              *last-offset (volatile! 0)
-              handle (fn []
-                       (let [scroll-top (js/parseInt (.-scrollTop container))
-                             offset (if (> (+ scroll-top head-top) el-top)
-                                      (+ (- scroll-top el-top) head-top 1) 0)
-                             offset (js/parseInt offset)
-                             last-offset @*last-offset]
-                         (if (and (not (zero? last-offset))
-                               (not= offset last-offset))
-                           (let [dir (if (neg? (- offset last-offset)) -1 1)]
-                             (loop [offset' (+ last-offset dir)]
-                               (translate offset')
-                               (if (and (not= offset offset')
-                                     (< (abs (- offset offset')) 100))
-                                 (recur (+ offset' dir))
-                                 (translate offset))))
-                           (translate offset))
-                         (vreset! *last-offset offset)))
-              handler (fn [^js e]
-                        (when (not @*ticking?)
-                          (js/window.requestAnimationFrame
-                            #(do (handle) (vreset! *ticking? false)))
-                          (vreset! *ticking? true)))]
-          (.addEventListener container "scroll" handler)
-          #(.removeEventListener container "scroll" handler)))
-      [])
+     (fn []
+       (let [^js container (get-main-scroll-container)
+             ^js el (rum/deref el-ref)
+             ^js cls (.-classList el)
+             *ticking? (volatile! false)
+             el-top (-> el (.getBoundingClientRect) (.-top))
+             head-top (-> (get-head-container) (js/getComputedStyle) (.-height) (js/parseInt))
+             translate (fn [offset]
+                         (set! (. (.-style el) -transform) (str "translate3d(0, " offset "px , 0)"))
+                         (if (zero? offset)
+                           (.remove cls "translated")
+                           (.add cls "translated")))
+             *last-offset (volatile! 0)
+             handle (fn []
+                      (let [scroll-top (js/parseInt (.-scrollTop container))
+                            offset (if (> (+ scroll-top head-top) el-top)
+                                     (+ (- scroll-top el-top) head-top 1) 0)
+                            offset (js/parseInt offset)
+                            last-offset @*last-offset]
+                        (if (and (not (zero? last-offset))
+                                 (not= offset last-offset))
+                          (let [dir (if (neg? (- offset last-offset)) -1 1)]
+                            (loop [offset' (+ last-offset dir)]
+                              (translate offset')
+                              (if (and (not= offset offset')
+                                       (< (abs (- offset offset')) 100))
+                                (recur (+ offset' dir))
+                                (translate offset))))
+                          (translate offset))
+                        (vreset! *last-offset offset)))
+             handler (fn [^js e]
+                       (when (not @*ticking?)
+                         (js/window.requestAnimationFrame
+                          #(do (handle) (vreset! *ticking? false)))
+                         (vreset! *ticking? true)))]
+         (.addEventListener container "scroll" handler)
+         #(.removeEventListener container "scroll" handler)))
+     [])
 
     [:div.charlie-table
      [:div.charlie-table-header

+ 6 - 0
deps/shui/src/logseq/shui/ui.cljs

@@ -110,6 +110,12 @@
 (def context-menu-sub-trigger (util/lsui-wrap "ContextMenuSubTrigger"))
 (def context-menu-radio-group (util/lsui-wrap "ContextMenuRadioGroup"))
 
+;; tabs
+(def tabs (util/lsui-wrap "Tabs"))
+(def tabs-list (util/lsui-wrap "TabsList"))
+(def tabs-trigger (util/lsui-wrap "TabsTrigger"))
+(def tabs-content (util/lsui-wrap "TabsContent"))
+
 (def dialog dialog-core/dialog)
 (def dialog-portal dialog-core/dialog-portal)
 (def dialog-overlay dialog-core/dialog-overlay)

+ 53 - 0
packages/ui/@/components/ui/tabs.tsx

@@ -0,0 +1,53 @@
+import * as React from "react"
+import * as TabsPrimitive from "@radix-ui/react-tabs"
+
+import { cn } from "@/lib/utils"
+
+const Tabs = TabsPrimitive.Root
+
+const TabsList = React.forwardRef<
+  React.ElementRef<typeof TabsPrimitive.List>,
+  React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
+>(({ className, ...props }, ref) => (
+  <TabsPrimitive.List
+    ref={ref}
+    className={cn(
+      "inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
+      className
+    )}
+    {...props}
+  />
+))
+TabsList.displayName = TabsPrimitive.List.displayName
+
+const TabsTrigger = React.forwardRef<
+  React.ElementRef<typeof TabsPrimitive.Trigger>,
+  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
+>(({ className, ...props }, ref) => (
+  <TabsPrimitive.Trigger
+    ref={ref}
+    className={cn(
+      "inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
+      className
+    )}
+    {...props}
+  />
+))
+TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
+
+const TabsContent = React.forwardRef<
+  React.ElementRef<typeof TabsPrimitive.Content>,
+  React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
+>(({ className, ...props }, ref) => (
+  <TabsPrimitive.Content
+    ref={ref}
+    className={cn(
+      "mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
+      className
+    )}
+    {...props}
+  />
+))
+TabsContent.displayName = TabsPrimitive.Content.displayName
+
+export { Tabs, TabsList, TabsTrigger, TabsContent }

+ 1 - 0
packages/ui/package.json

@@ -30,6 +30,7 @@
     "@radix-ui/react-slider": "^1.1.2",
     "@radix-ui/react-slot": "^1.1.0",
     "@radix-ui/react-switch": "^1.0.3",
+    "@radix-ui/react-tabs": "^1.1.1",
     "@radix-ui/react-toast": "^1.1.5",
     "@radix-ui/react-toggle": "^1.0.3",
     "@radix-ui/react-toggle-group": "^1.0.4",

+ 3 - 1
packages/ui/src/ui.ts

@@ -91,6 +91,7 @@ import { Toggle } from '@/components/ui/toggle'
 import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
 import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
 import * as uniqolor from 'uniqolor'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
 
 declare global {
   var LSUI: any
@@ -185,6 +186,7 @@ const shadui = {
   TooltipContent, TooltipProvider, TooltipPortal,
   Toggle, ToggleGroup, ToggleGroupItem,
   Avatar, AvatarImage, AvatarFallback,
+  Tabs, TabsContent, TabsList, TabsTrigger
 }
 
 function setupGlobals() {
@@ -203,4 +205,4 @@ setupGlobals()
 
 export {
   setupGlobals
-}
+}

+ 72 - 6
packages/ui/yarn.lock

@@ -2267,6 +2267,16 @@
     "@radix-ui/react-primitive" "1.0.3"
     "@radix-ui/react-slot" "1.0.2"
 
+"@radix-ui/[email protected]":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-collection/-/react-collection-1.1.0.tgz#f18af78e46454a2360d103c2251773028b7724ed"
+  integrity sha512-GZsZslMJEyo1VKm5L1ZJY8tGDxZNPAoUeQUIbKeJfoi7Q4kmig5AsgLMYYuyYbfjd8fBmFORAIwYAkXMnXZgZw==
+  dependencies:
+    "@radix-ui/react-compose-refs" "1.1.0"
+    "@radix-ui/react-context" "1.1.0"
+    "@radix-ui/react-primitive" "2.0.0"
+    "@radix-ui/react-slot" "1.1.0"
+
 "@radix-ui/[email protected]":
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
@@ -2392,6 +2402,11 @@
   dependencies:
     "@babel/runtime" "^7.13.10"
 
+"@radix-ui/[email protected]":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-direction/-/react-direction-1.1.0.tgz#a7d39855f4d077adc2a1922f9c353c5977a09cdc"
+  integrity sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==
+
 "@radix-ui/[email protected]":
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz#35b7826fa262fd84370faef310e627161dffa76b"
@@ -2753,6 +2768,21 @@
     "@radix-ui/react-use-callback-ref" "1.0.1"
     "@radix-ui/react-use-controllable-state" "1.0.1"
 
+"@radix-ui/[email protected]":
+  version "1.1.0"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.0.tgz#b30c59daf7e714c748805bfe11c76f96caaac35e"
+  integrity sha512-EA6AMGeq9AEeQDeSH0aZgG198qkfHSbvWTf1HvoDmOB5bBG/qTxjYMWUKMnYiV6J/iP/J8MEFSuB2zRU2n7ODA==
+  dependencies:
+    "@radix-ui/primitive" "1.1.0"
+    "@radix-ui/react-collection" "1.1.0"
+    "@radix-ui/react-compose-refs" "1.1.0"
+    "@radix-ui/react-context" "1.1.0"
+    "@radix-ui/react-direction" "1.1.0"
+    "@radix-ui/react-id" "1.1.0"
+    "@radix-ui/react-primitive" "2.0.0"
+    "@radix-ui/react-use-callback-ref" "1.1.0"
+    "@radix-ui/react-use-controllable-state" "1.1.0"
+
 "@radix-ui/react-select@^1.2.2":
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-1.2.2.tgz#caa981fa0d672cf3c1b2a5240135524e69b32181"
@@ -2879,6 +2909,20 @@
     "@radix-ui/react-use-previous" "1.0.1"
     "@radix-ui/react-use-size" "1.0.1"
 
+"@radix-ui/react-tabs@^1.1.1":
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/@radix-ui/react-tabs/-/react-tabs-1.1.1.tgz#698bd97923f6bcd629738198a73beebcc4c88b30"
+  integrity sha512-3GBUDmP2DvzmtYLMsHmpA1GtR46ZDZ+OreXM/N+kkQJOPIgytFWWTfDQmBQKBvaFS0Vno0FktdbVzN28KGrMdw==
+  dependencies:
+    "@radix-ui/primitive" "1.1.0"
+    "@radix-ui/react-context" "1.1.1"
+    "@radix-ui/react-direction" "1.1.0"
+    "@radix-ui/react-id" "1.1.0"
+    "@radix-ui/react-presence" "1.1.1"
+    "@radix-ui/react-primitive" "2.0.0"
+    "@radix-ui/react-roving-focus" "1.1.0"
+    "@radix-ui/react-use-controllable-state" "1.1.0"
+
 "@radix-ui/react-toast@^1.1.5":
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.5.tgz#f5788761c0142a5ae9eb97f0051fd3c48106d9e6"
@@ -8956,8 +9000,7 @@ stream-shift@^1.0.0:
   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
   integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
 
-"string-width-cjs@npm:string-width@^4.2.0", string-width@^4, string-width@^4.1.0, string-width@^4.2.0, string-width@^5.0.1, string-width@^5.1.2:
-  name string-width-cjs
+"string-width-cjs@npm:string-width@^4.2.0":
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -8975,6 +9018,15 @@ [email protected]:
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.0.0"
 
+string-width@^4, string-width@^4.1.0, string-width@^4.2.0, string-width@^5.0.1, string-width@^5.1.2:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.1"
+
 string_decoder@^1.1.1:
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -8989,8 +9041,14 @@ string_decoder@~1.1.1:
   dependencies:
     safe-buffer "~5.1.0"
 
-"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
-  name strip-ansi-cjs
+"strip-ansi-cjs@npm:strip-ansi@^6.0.1":
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
+strip-ansi@^6, strip-ansi@^6.0.0, strip-ansi@^6.0.1, strip-ansi@^7.0.1:
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -9650,8 +9708,16 @@ wordwrap@^1.0.0:
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
   integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
 
-"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0:
-  name wrap-ansi-cjs
+"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0":
+  version "7.0.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
+  integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
+wrap-ansi@^7.0.0:
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==

+ 3 - 3
resources/package.json

@@ -43,8 +43,7 @@
     "posthog-js": "1.10.2",
     "semver": "7.5.2",
     "socks-proxy-agent": "8.0.2",
-    "update-electron-app": "2.0.1",
-    "electron-devtools-installer": "3.2.0"
+    "update-electron-app": "2.0.1"
   },
   "devDependencies": {
     "@electron-forge/cli": "^7.3.1",
@@ -57,7 +56,8 @@
     "@electron/rebuild": "3.2.10",
     "electron": "31.7.5",
     "electron-builder": "25.1.8",
-    "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git"
+    "electron-forge-maker-appimage": "https://github.com/logseq/electron-forge-maker-appimage.git",
+    "electron-devtools-installer": "^3.2.0"
   },
   "resolutions": {
     "**/electron": "31.7.5",

+ 5 - 1
scripts/src/logseq/tasks/db_graph/create_graph_with_schema_org.cljs

@@ -366,6 +366,7 @@
                                              {:logseq.property.class/properties [:block/title]}
                                              {:property/schema.classes [:block/title]}
                                              {:logseq.property/parent [:block/title]}
+                                             {:block/tags [:block/title]}
                                              {:block/refs [:block/title]}]) ...]
                             :in $
                             :where [?b :db/ident ?ident]]
@@ -376,11 +377,12 @@
                             (map (fn [m]
                                    (let [props (->> (db-property/properties m)
                                                     (into {}))]
-                                     (cond-> (select-keys m [:block/name :block/type :block/title :block/schema :db/ident
+                                     (cond-> (select-keys m [:block/name :block/tags :block/title :block/schema :db/ident
                                                              :logseq.property.class/properties :logseq.property/parent
                                                              :db/cardinality :property/schema.classes :block/refs])
                                        (seq props)
                                        (assoc :block/properties (-> (update-keys props name)
+                                                                    (dissoc "tags")
                                                                     (update-vals (fn [v]
                                                                                    (if (:db/id v)
                                                                                      (db-property/property-value-content (d/entity db (:db/id v)))
@@ -391,6 +393,8 @@
                                        (update :logseq.property/parent :block/title)
                                        (seq (:property/schema.classes m))
                                        (update :property/schema.classes #(set (map :block/title %)))
+                                       (seq (:block/tags m))
+                                       (update :block/tags #(set (map :block/title %)))
                                        (seq (:block/refs m))
                                        (update :block/refs #(set (map :block/title %)))))))
                             set)))))

+ 2 - 1
scripts/src/logseq/tasks/dev/db_and_file_graphs.clj

@@ -91,7 +91,8 @@
   (let [file-concepts (->>
                        ;; from logseq.db.frontend.schema
                        [:block/namespace :block/properties-text-values :block/pre-block :recent/pages :block/file :block/properties-order
-                        :block/repeated :block/deadline :block/scheduled :block/priority :block/marker :block/macros]
+                        :block/repeated :block/deadline :block/scheduled :block/priority :block/marker :block/macros
+                        :block/type]
                        (map str)
                        (into [;; e.g. block/properties :title
                               "block/properties :"

+ 8 - 7
src/main/frontend/components/all_pages.cljs

@@ -21,13 +21,14 @@
          :cell (fn [_table row _column]
                  (component-block/page-cp {} row))
          :type :string}
-        {:id :block/type
-         :name "Type"
-         :cell (fn [_table row _column]
-                 (let [type (get row :block/type)]
-                   [:div.capitalize (if (= type "class") "tag" type)]))
-         :get-value (fn [row] (get row :block/type))
-         :type :string}
+        (when (not (config/db-based-graph? (state/get-current-repo)))
+          {:id :block/type
+           :name "Page type"
+           :cell (fn [_table row _column]
+                   (let [type (get row :block/type)]
+                     [:div.capitalize type]))
+           :get-value (fn [row] (get row :block/type))
+           :type :string})
         {:id :block.temp/refs-count
          :name (t :page/backlinks)
          :cell (fn [_table row _column] (:block.temp/refs-count row))

+ 35 - 26
src/main/frontend/components/block.cljs

@@ -20,7 +20,6 @@
             [frontend.components.query :as query]
             [frontend.components.query.builder :as query-builder-component]
             [frontend.components.svg :as svg]
-            [frontend.components.title :as title]
             [frontend.components.select :as select]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
@@ -701,7 +700,9 @@
      (when (and show-icon? (not tag?))
        (let [own-icon (get page-entity (pu/get-pid :logseq.property/icon))
              emoji? (and (map? own-icon) (= (:type own-icon) :emoji))]
-         (when-let [icon (icon-component/get-node-icon-cp page-entity {:color? true :not-text-or-page? true})]
+         (when-let [icon (icon-component/get-node-icon-cp page-entity {:color? true
+                                                                       :not-text-or-page? true
+                                                                       :own-icon? true})]
            [:span {:class (str "icon-emoji-wrap " (when emoji? "as-emoji"))}
             icon])))
      [:span
@@ -722,7 +723,7 @@
                                (->elem :span (map-inline config label))
 
                                show-unique-title?
-                               (title/block-unique-title page-entity)
+                               (block-handler/block-unique-title page-entity)
 
                                :else
                                (let [title (:block/title page-entity)
@@ -755,7 +756,7 @@
                                          (db-content/content-id-ref->page s (:block/refs page-entity))
                                          :else
                                          s)
-                                     s (if tag? (str "#" s) s)]
+                                     s (if (and tag? (not (:hide-tag-symbol? config))) (str "#" s) s)]
                                  (if (ldb/page? page-entity)
                                    s
                                    (block-title config page-entity))))]
@@ -2588,8 +2589,9 @@
 (rum/defcs block-tag <
   (rum/local false ::hover?)
   [state block tag config popup-opts]
-  (let [*hover? (::hover? state)]
-    [:div.block-tag
+  (let [*hover? (::hover? state)
+        hover? @*hover?]
+    [:div.block-tag.items-center
      {:key (str "tag-" (:db/id tag))
       :on-mouse-over #(reset! *hover? true)
       :on-mouse-out #(reset! *hover? false)
@@ -2614,18 +2616,23 @@
                                :on-click #(db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag))}
                               "Remove tag")])
                           popup-opts))}
+     (if (and hover? (not (ldb/private-tags (:db/ident tag))))
+       [:a.inline.close.flex.transition-opacity.duration-300.ease-in
+        {:class (if @*hover? "!opacity-100" "!opacity-0")
+         :title "Remove this tag"
+         :on-pointer-down
+         (fn [e]
+           (util/stop e)
+           (db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag)))}
+        (ui/icon "x" {:size 14
+                      :style {:margin-top 1}})]
+       [:a.hash-symbol {:style {:margin-left 5}}
+        "#"])
      (page-cp (assoc config
+                     :disable-preview? true
                      :tag? true
-                     :disable-preview? true)
-              tag)
-     [:a.close.flex.transition-opacity.duration-300.ease-in
-      {:class (if @*hover? "!opacity-100" "!opacity-0")
-       :title "Remove this tag"
-       :on-pointer-down
-       (fn [e]
-         (util/stop e)
-         (db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag)))}
-      (ui/icon "x" {:size 15})]]))
+                     :hide-tag-symbol? true)
+              tag)]))
 
 (rum/defc tags-cp
   "Tags without inline or hidden tags"
@@ -2635,14 +2642,15 @@
                       (:block/tags block)
                       (remove (fn [t]
                                 (or (ldb/inline-tag? (:block/raw-title block) t)
-                                    (:logseq.property.class/hide-from-node t)))))
+                                    (:logseq.property.class/hide-from-node t)
+                                    (contains? ldb/internal-tags (:db/ident t))))))
           popup-opts {:align :end
                       :content-props {:on-click (fn [] (shui/popup-hide!))
                                       :class "w-60"}}
           tags-count (count block-tags)]
       (when (seq block-tags)
         (if (< tags-count 3)
-          [:div.block-tags
+          [:div.block-tags.gap-1
            (for [tag block-tags]
              (rum/with-key
                (block-tag block tag config popup-opts)
@@ -2653,13 +2661,14 @@
                                                  (fn []
                                                    (for [tag block-tags]
                                                      [:div.flex.flex-row.items-center.gap-1
-                                                      (shui/button
-                                                       {:title "Remove tag"
-                                                        :variant :ghost
-                                                        :class "!p-1 text-muted-foreground"
-                                                        :size :sm
-                                                        :on-click #(db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag))}
-                                                       (ui/icon "X" {:size 14}))
+                                                      (when-not (ldb/private-tags (:db/ident tag))
+                                                        (shui/button
+                                                         {:title "Remove tag"
+                                                          :variant :ghost
+                                                          :class "!p-1 text-muted-foreground"
+                                                          :size :sm
+                                                          :on-click #(db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag))}
+                                                         (ui/icon "X" {:size 14})))
                                                       (page-cp (assoc config
                                                                       :tag? true
                                                                       :disable-preview? true
@@ -2672,7 +2681,7 @@
                               :tag? true
                               :disable-preview? true
                               :disable-click? true) tag)])
-           [:div.text-sm.opacity-50
+           [:div.text-sm.opacity-50.ml-1
             (str "+" (- tags-count 2))]])))))
 
 (rum/defc block-positioned-properties

+ 8 - 4
src/main/frontend/components/block.css

@@ -949,7 +949,7 @@ html.is-mac {
 
 .positioned-properties.block-right {
   button {
-    @apply whitespace-nowrap mr-0.5;
+    @apply whitespace-nowrap;
   }
 
   .block-title-wrap {
@@ -970,14 +970,18 @@ html.is-mac {
 }
 
 .block-tag {
-  @apply pr-1 flex flex-row items-center gap-1;
+  @apply flex flex-row items-center;
 }
 
 .block-tag a.tag {
   @apply flex text-sm font-normal items-center opacity-70;
 }
 
-.block-tag a.tag:hover {
+.block-tag a.hash-symbol {
+    @apply text-sm text-sm font-normal opacity-70;
+}
+
+.block-tag a.tag:hover, .block-tag a.hash-symbol:hover {
   @apply opacity-100;
 }
 
@@ -1013,7 +1017,7 @@ html.is-mac {
 }
 
 .ls-page-title .block-tags {
-  @apply relative -right-1 min-h-full;
+  @apply relative min-h-full;
 }
 
 .ls-code-editor-wrap {

+ 3 - 3
src/main/frontend/components/cmdk/core.cljs

@@ -4,7 +4,6 @@
             [electron.ipc :as ipc]
             [frontend.components.block :as block]
             [frontend.components.cmdk.list-item :as list-item]
-            [frontend.components.title :as title]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.db :as db]
@@ -18,6 +17,7 @@
             [frontend.handler.page :as page-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
+            [frontend.handler.block :as block-handler]
             [frontend.mixins :as mixins]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.modules.shortcut.utils :as shortcut-utils]
@@ -235,7 +235,7 @@
                "whiteboard"
                :else
                "page")
-        title (title/block-unique-title page)
+        title (block-handler/block-unique-title page)
         title' (if source-page (str title " -> alias: " (:block/title source-page)) title)]
     (hash-map :icon icon
               :icon-theme :gray
@@ -245,7 +245,7 @@
 (defn- block-item
   [repo block current-page !input]
   (let [id (:block/uuid block)
-        text (title/block-unique-title block)
+        text (block-handler/block-unique-title block)
         icon "letter-n"]
     {:icon icon
      :icon-theme :gray

+ 4 - 4
src/main/frontend/components/container.cljs

@@ -15,7 +15,6 @@
             [frontend.components.block :as block]
             [dommy.core :as d]
             [frontend.components.content :as cp-content]
-            [frontend.components.title :as title]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t tt]]
             [frontend.db :as db]
@@ -32,6 +31,7 @@
             [frontend.handler.user :as user-handler]
             [frontend.handler.whiteboard :as whiteboard-handler]
             [frontend.handler.recent :as recent-handler]
+            [frontend.handler.block :as block-handler]
             [frontend.mixins :as mixins]
             [frontend.mobile.action-bar :as action-bar]
             [frontend.mobile.footer :as footer]
@@ -139,7 +139,7 @@
                                                              :class "w-60"}})
                           (util/stop e))}
        (ldb/object? page)
-       (assoc :title (title/block-unique-title page)))
+       (assoc :title (block-handler/block-unique-title page)))
      [:span.page-icon icon]
      [:span.page-title {:class (when untitled? "opacity-50")
                         :style {:display "ruby"}}
@@ -214,7 +214,7 @@
                db-based?
                (concat [:tag/tasks :tag/assets])
                (not db-based?)
-               (#(cons :whiteboards %)) )
+               (#(cons :whiteboards %)))
         [checked-navs set-checked-navs!] (rum/use-state (or (storage/get :ls-sidebar-navigations)
                                                             [:whiteboards :flashcards :graph-view :all-pages]))]
 
@@ -362,7 +362,7 @@
       (for [page pages]
         [:li.recent-item.select-none.font-medium
          {:key (str "recent-" (:db/id page))
-          :title (title/block-unique-title page)
+          :title (block-handler/block-unique-title page)
           :draggable true
           :on-drag-start (fn [event] (editor-handler/block->data-transfer! (:block/name page) event true))
           :data-ref name}

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

@@ -446,7 +446,7 @@
   flex: 1;
 
   .page {
-    @apply px-6;
+    @apply px-4;
   }
 }
 

+ 15 - 14
src/main/frontend/components/db_based/page.cljs

@@ -4,21 +4,22 @@
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
             [logseq.shui.ui :as shui]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [frontend.util :as util]))
 
 (rum/defc configure-property < rum/reactive db-mixins/query
   [page]
   (let [page (db/sub-block (:db/id page))]
-    [:div.pb-4.-ml-1
-     (shui/button
-      {:variant "ghost"
-       :class "opacity-50 hover:opacity-90"
-       :size :sm
-       :on-click (fn [^js e]
-                   (shui/popup-show! (.-target e)
-                                     (fn []
-                                       (property-config/dropdown-editor page nil {:debug? (.-altKey e)}))
-                                     {:content-props {:class "ls-property-dropdown-editor as-root"}
-                                      :align "start"
-                                      :as-dropdown? true}))}
-      "Configure property")]))
+    (shui/tabs-trigger
+     {:value "configure"
+      :class "py-1 text-xs"
+      :on-pointer-down (fn [e]
+                         (util/stop e))
+      :on-click (fn [^js e]
+                  (shui/popup-show! (.-target e)
+                                    (fn []
+                                      (property-config/dropdown-editor page nil {:debug? (.-altKey e)}))
+                                    {:content-props {:class "ls-property-dropdown-editor as-root"}
+                                     :align "start"
+                                     :as-dropdown? true}))}
+     "Configure property")))

+ 10 - 4
src/main/frontend/components/editor.cljs

@@ -6,7 +6,6 @@
             [frontend.components.file-based.datetime :as datetime-comp]
             [frontend.components.search :as search]
             [frontend.components.svg :as svg]
-            [frontend.components.title :as title]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
@@ -19,6 +18,7 @@
             [frontend.handler.paste :as paste-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.search :as search-handler]
+            [frontend.handler.block :as block-handler]
             [frontend.mixins :as mixins]
             [frontend.search :refer [fuzzy-search]]
             [frontend.state :as state]
@@ -35,7 +35,8 @@
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [react-draggable]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [logseq.db.frontend.class :as db-class]))
 
 (defn filter-commands
   [page? commands]
@@ -156,7 +157,12 @@
                            ;; reorder, shortest and starts-with first.
                            (let [matched-pages-with-new-page
                                  (fn [partial-matched-pages]
-                                   (if (or (db/page-exists? q (if db-tag? "class" "page"))
+                                   (if (or (db/page-exists? q (if db-tag?
+                                                                #{:logseq.class/Tag}
+                                                                ;; Page existence here should be the same as entity-util/page?.
+                                                                ;; Don't show 'New page' if a page has any of these tags
+                                                                (into #{:logseq.class/Page :logseq.class/Tag :logseq.class/Property}
+                                                                      db-class/page-children-classes)))
                                            (and db-tag? (some ldb/class? (:block/_alias (db/get-page q)))))
                                      partial-matched-pages
                                      (if db-tag?
@@ -215,7 +221,7 @@
                                             (if (ldb/class? target)
                                               (str (:block/title block) " -> alias: " (:block/title target))
                                               (:block/title block)))
-                                          (title/block-unique-title block))]
+                                          (block-handler/block-unique-title block))]
                               (search-handler/highlight-exact-query title q))]]))
          :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (if db-tag?
                                                                     "Search for a tag"

+ 1 - 1
src/main/frontend/components/file_sync.cljs

@@ -693,7 +693,7 @@
      [:div.cp__file-sync-page-histories-right
       [:h1.title.text-xl
        "Current version"]
-      (page/page-blocks-cp (state/get-current-repo) page-entity nil)]
+      (page/page-blocks-cp page-entity nil)]
 
      ;; ready loading
      [:div.flex.items-center.h-full.justify-center.w-full.absolute.ready-loading

+ 3 - 1
src/main/frontend/components/icon.cljs

@@ -61,7 +61,9 @@
 (defn get-node-icon-cp
   [node-entity opts]
   (let [opts' (merge {:size 14} opts)
-        node-icon (get-node-icon node-entity)]
+        node-icon (if (:own-icon? opts)
+                    (get node-entity (pu/get-pid :logseq.property/icon))
+                    (get-node-icon node-entity))]
     (when-not (or (string/blank? node-icon) (and (contains? #{"letter-n" "page"} node-icon) (:not-text-or-page? opts)))
       [:div.icon-cp-container.flex.items-center
        (merge {:style {:color (or (:color node-icon) "inherit")}}

+ 64 - 75
src/main/frontend/components/objects.cljs

@@ -154,53 +154,45 @@
 
     (if loading?
       (ui/skeleton)
-      [:div.flex.flex-col.gap-2.mt-2
-
-       (ui/foldable
-        [:div.font-medium.opacity-60.as-toggle
-         "Tagged Nodes"]
-         (fn []
-           [:div.mt-2
-            (views/view view-entity {:config config
-                                     :data data
-                                     :set-data! set-data!
-                                     :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]
-                                                          (shui/dialog-open!
-                                                            (fn []
-                                                              [:div.flex.flex-col.gap-2
-                                                               [:div.font-medium "Add assets"]
-                                                               (filepicker/picker
-                                                                 {:on-change (fn [_e files]
-                                                                               (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!))
-                                     :show-add-property? true
-                                     :add-property! (fn []
-                                                      (state/pub-event! [:editor/new-property {:block class
-                                                                                               :class-schema? true}]))
-                                     :on-delete-rows (fn [table selected-rows]
-                                                       (let [pages (filter ldb/page? selected-rows)
-                                                             blocks (remove ldb/page? selected-rows)]
-                                                         (p/do!
-                                                           (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}))))
-                                                           (set-data! (get-class-objects class))
-                                                           (when-let [f (get-in table [:data-fns :set-row-selection!])]
-                                                             (f {})))))})])
-         {:disable-on-pointer-down? true
-         :default-collapsed? (:sidebar? config)})])))
+      (views/view view-entity
+                  {:config config
+                   :data data
+                   :set-data! set-data!
+                   :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]
+                                        (shui/dialog-open!
+                                         (fn []
+                                           [:div.flex.flex-col.gap-2
+                                            [:div.font-medium "Add assets"]
+                                            (filepicker/picker
+                                             {:on-change (fn [_e files]
+                                                           (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!))
+                   :show-add-property? true
+                   :add-property! (fn []
+                                    (state/pub-event! [:editor/new-property {:block class
+                                                                             :class-schema? true}]))
+                   :on-delete-rows (fn [table selected-rows]
+                                     (let [pages (filter ldb/page? selected-rows)
+                                           blocks (remove ldb/page? selected-rows)]
+                                       (p/do!
+                                        (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}))))
+                                        (set-data! (get-class-objects class))
+                                        (when-let [f (get-in table [:data-fns :set-row-selection!])]
+                                          (f {})))))}))))
 
 (rum/defcs class-objects < rum/reactive db-mixins/query mixins/container-id
   [state class {:keys [current-page? sidebar?]}]
@@ -251,35 +243,32 @@
      [])
 
     (when (false? loading?)
-      (ui/foldable
-       [:div.font-medium.opacity-50 "Nodes with Property"]
-       [:div.mt-2
-        (views/view view-entity {:config config
-                                 :data data
-                                 :set-data! set-data!
-                                 :title-key :views.table/property-nodes
-                                 :columns columns
-                                 :add-new-object! #(add-new-property-object! property set-data!)
+      (views/view view-entity
+                  {:config config
+                   :data data
+                   :set-data! set-data!
+                   :title-key :views.table/property-nodes
+                   :columns columns
+                   :add-new-object! #(add-new-property-object! property set-data!)
                                ;; TODO: Add support for adding column
-                                 :show-add-property? false
-                                 :on-delete-rows (when-not (contains? #{:logseq.property/built-in? :logseq.property/parent}
-                                                                      (:db/ident property))
-                                                   (fn [table selected-rows]
-                                                     (let [pages (filter ldb/page? selected-rows)
-                                                           blocks (remove ldb/page? selected-rows)]
-                                                       (p/do!
-                                                        (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 (:db/ident property)]) page-ids)]
-                                                           (when (seq tx-data)
-                                                             (outliner-op/transact! tx-data {:outliner-op :save-block}))))
-                                                        (set-data! (get-property-related-objects (state/get-current-repo) property))
-                                                        (when-let [f (get-in table [:data-fns :set-row-selection!])]
-                                                          (f {}))))))})]
-       {:disable-on-pointer-down? true}))))
+                   :show-add-property? false
+                   :on-delete-rows (when-not (contains? #{:logseq.property/built-in? :logseq.property/parent}
+                                                        (:db/ident property))
+                                     (fn [table selected-rows]
+                                       (let [pages (filter ldb/page? selected-rows)
+                                             blocks (remove ldb/page? selected-rows)]
+                                         (p/do!
+                                          (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 (:db/ident property)]) page-ids)]
+                                             (when (seq tx-data)
+                                               (outliner-op/transact! tx-data {:outliner-op :save-block}))))
+                                          (set-data! (get-property-related-objects (state/get-current-repo) property))
+                                          (when-let [f (get-in table [:data-fns :set-row-selection!])]
+                                            (f {}))))))}))))
 
 ;; Show all nodes containing the given property
 (rum/defcs property-related-objects < rum/reactive db-mixins/query mixins/container-id

+ 104 - 56
src/main/frontend/components/page.cljs

@@ -196,7 +196,7 @@
 
 (rum/defcs page-blocks-cp < rum/reactive db-mixins/query
   {:will-mount (fn [state]
-                 (let [page-e (second (:rum/args state))
+                 (let [page-e (first (:rum/args state))
                        page-name (:block/name page-e)]
                    (when (and page-name
                               (db/journal-page? page-name)
@@ -204,7 +204,7 @@
                                   (date/journal-title->int (date/today))))
                      (state/pub-event! [:journal/insert-template page-name])))
                  state)}
-  [state repo page-e {:keys [sidebar? whiteboard?] :as config}]
+  [state page-e {:keys [sidebar? whiteboard?] :as config}]
   (when page-e
     (let [page-name (or (:block/name page-e)
                         (str (:block/uuid page-e)))
@@ -220,44 +220,35 @@
                      (remove (fn [b] (some? (get b (:db/ident block)))) children)
 
                      :else
-                     children)
-          db-based? (config/db-based-graph? repo)]
-      [:<>
-       (let [blocks (cond
-                      (and
-                       (not block?)
-                       (empty? children) page-e)
-                      (dummy-block page-e)
-
-                      :else
-                      (let [document-mode? (state/sub :document/mode?)
-                            hiccup-config (merge
-                                           {:id (if block? (str block-id) page-name)
-                                            :db/id (:db/id block)
-                                            :block? block?
-                                            :editor-box editor/box
-                                            :document/mode? document-mode?}
-                                           config)
-                            config (common-handler/config-with-document-mode hiccup-config)
-                            blocks (if block? [block] (db/sort-by-order children block))]
-                        (let [add-button? (not (or config/publishing?
-                                                   (let [last-child-id (model/get-block-deep-last-open-child-id (db/get-db) (:db/id (last blocks)))
-                                                         block' (if last-child-id (db/entity last-child-id) (last blocks))]
-                                                     (string/blank? (:block/title block')))))]
-                          [:div
-                           {:class (when add-button? "show-add-button")}
-                           (page-blocks-inner page-e blocks config sidebar? whiteboard? block-id)
-                           (let [args (if block-id
-                                        {:block-uuid block-id}
-                                        {:page page-name})]
-                             (add-button args (:container-id config)))])))]
-         (if (and db-based? (or (ldb/class? block) (ldb/property? block)))
-           [:div.mt-4.ml-2.-mb-1
-            (ui/foldable
-             [:div.font-medium.as-toggle {:class "pl-0.5"} "Notes"]
-             [:div.ml-1.-mb-2 blocks]
-             {:disable-on-pointer-down? true})]
-           blocks))])))
+                     children)]
+      (cond
+        (and
+         (not block?)
+         (empty? children) page-e)
+        (dummy-block page-e)
+
+        :else
+        (let [document-mode? (state/sub :document/mode?)
+              hiccup-config (merge
+                             {:id (if block? (str block-id) page-name)
+                              :db/id (:db/id block)
+                              :block? block?
+                              :editor-box editor/box
+                              :document/mode? document-mode?}
+                             config)
+              config (common-handler/config-with-document-mode hiccup-config)
+              blocks (if block? [block] (db/sort-by-order children block))]
+          (let [add-button? (not (or config/publishing?
+                                     (let [last-child-id (model/get-block-deep-last-open-child-id (db/get-db) (:db/id (last blocks)))
+                                           block' (if last-child-id (db/entity last-child-id) (last blocks))]
+                                       (string/blank? (:block/title block')))))]
+            [:div
+             {:class (when add-button? "show-add-button")}
+             (page-blocks-inner page-e blocks config sidebar? whiteboard? block-id)
+             (let [args (if block-id
+                          {:block-uuid block-id}
+                          {:page page-name})]
+               (add-button args (:container-id config)))]))))))
 
 (rum/defc today-queries < rum/reactive
   [repo today? sidebar?]
@@ -301,10 +292,11 @@
 (rum/defc page-title-editor < rum/reactive
   [page {:keys [*input-value *title-value *edit? untitled? page-name old-name whiteboard-page?]}]
   (let [input-ref (rum/create-ref)
+        tag-idents (map :db/ident (:block/tags page))
         collide? #(and (not= (util/page-name-sanity-lc page-name)
                              (util/page-name-sanity-lc @*title-value))
-                       (db/page-exists? page-name (:block/type page))
-                       (db/page-exists? @*title-value (:block/type page)))
+                       (db/page-exists? page-name tag-idents)
+                       (db/page-exists? @*title-value tag-idents))
         rollback-fn #(let [old-name (if untitled? "" old-name)]
                        (reset! *title-value old-name)
                        (gobj/set (rum/deref input-ref) "value" old-name)
@@ -559,6 +551,65 @@
        (plugins/hook-ui-slot :page-head-actions-slotted nil)
        (plugins/hook-ui-items :pagebar)])))
 
+(rum/defc tabs
+  [page opts]
+  (let [class? (ldb/class? page)
+        property? (ldb/property? page)
+        both? (and class? property?)
+        default-tab (cond
+                      both?
+                      "tag"
+                      class?
+                      "tag"
+                      :else
+                      "property")]
+    [:div.page-tabs
+     (shui/tabs
+      {:defaultValue default-tab
+       :class (str "w-full")}
+      (when (or both? property?)
+        [:div.flex.flex-row.gap-1.items-center
+         (shui/tabs-list
+          {:class "h-8"}
+          (when class?
+            (shui/tabs-trigger
+             {:value "tag"
+              :class "py-1 text-xs"}
+             "Tagged nodes"))
+          (when property?
+            (shui/tabs-trigger
+             {:value "property"
+              :class "py-1 text-xs"}
+             "Nodes with property"))
+          (when property?
+            (db-page/configure-property page)))])
+
+      (when class?
+        (shui/tabs-content
+         {:value "tag"}
+         (objects/class-objects page opts)))
+      (when property?
+        (shui/tabs-content
+         {:value "property"}
+         (objects/property-related-objects page (:current-page? opts)))))]))
+
+(rum/defc sidebar-page-properties
+  [config page]
+  (let [[collapsed? set-collapsed!] (rum/use-state true)]
+    [:div.ls-sidebar-page-properties.flex.flex-col.gap-2.mt-2
+     [:div
+      (shui/button
+       {:variant :ghost
+        :size :sm
+        :class "px-1 text-muted-foreground"
+        :on-click #(set-collapsed! (not collapsed?))}
+       [:span.text-xs (str (if collapsed? "Open" "Hide")) " properties"])]
+
+     (when-not collapsed?
+       [:<>
+        (component-block/db-properties-cp config page {:sidebar-properties? true})
+        [:hr.my-4]])]))
+
 ;; A page is just a logical block
 (rum/defcs ^:large-vars/cleanup-todo page-inner < rum/reactive db-mixins/query mixins/container-id
   (rum/local false ::all-collapsed?)
@@ -605,9 +656,8 @@
 
            (if (and whiteboard-page? (not sidebar?))
              [:div ((state/get-component :whiteboard/tldraw-preview) (:block/uuid page))] ;; FIXME: this is not reactive
-             [:div.relative.page-inner
-              (when (or (and db-based? (not block?))
-                        (and (not db-based?) (not sidebar?) (not block?)))
+             [:div.relative.grid.gap-8.page-inner
+              (when-not (or block? sidebar?)
                 [:div.flex.flex-row.space-between
                  (when (and (or (mobile-util/native-platform?) (util/mobile?)) (not db-based?))
                    [:div.flex.flex-row.pr-2
@@ -625,14 +675,9 @@
                                           :preview? preview?})))
                  (lsp-pagebar-slot)])
 
-              (when (and db-based? (ldb/property? page))
-                (db-page/configure-property page))
-
-              (when (and db-based? class-page?)
-                (objects/class-objects page {:current-page? option :sidebar? sidebar?}))
-
-              (when (and db-based? (ldb/property? page))
-                (objects/property-related-objects page (:current-page? option)))
+              (when (and db-based? sidebar?)
+                [:div.-mb-8
+                 (sidebar-page-properties config page)])
 
               (when (and block? (not sidebar?) (not whiteboard?))
                 (let [config (merge config {:id "block-parent"
@@ -640,10 +685,13 @@
                   [:div.mb-4
                    (component-block/breadcrumb config repo block-id {:level-limit 3})]))
 
+              (when (and db-based? (or class-page? (ldb/property? page)))
+                (tabs page {:current-page? option :sidebar? sidebar?}))
+
               [:div.ls-page-blocks
-               (page-blocks-cp repo page (merge option {:sidebar? sidebar?
-                                                        :container-id (:container-id state)
-                                                        :whiteboard? whiteboard?}))]])
+               (page-blocks-cp page (merge option {:sidebar? sidebar?
+                                                   :container-id (:container-id state)
+                                                   :whiteboard? whiteboard?}))]])
 
            (when (not preview?)
              [:div {:style {:padding-left 9}}

+ 2 - 4
src/main/frontend/components/page.css

@@ -65,8 +65,6 @@
   @apply rounded-sm;
 
   &.title {
-    @apply mb-3;
-
     .block-main-container {
       @apply gap-2;
     }
@@ -258,5 +256,5 @@ html.is-native-ios {
 }
 
 .ls-page-blocks {
-  @apply min-h-[60px] -mb-2;
-}
+  @apply min-h-[60px];
+}

+ 95 - 45
src/main/frontend/components/property.cljs

@@ -13,6 +13,7 @@
             [frontend.handler.property.util :as pu]
             [frontend.config :as config]
             [frontend.db :as db]
+            [frontend.db.model :as db-model]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.async :as db-async]
             [frontend.handler.db-based.property :as db-property-handler]
@@ -99,7 +100,8 @@
                       add-class-property? (and (ldb/class? block) class-schema?)]
                 (when *property (reset! *property property))
                 (p/do!
-                 (when *show-new-property-config? (reset! *show-new-property-config? false))
+                 (when *show-new-property-config?
+                   (reset! *show-new-property-config? false))
                  (when (= (:type schema) :node) (reset! *show-class-select? true))
                  (components-pu/update-property! property property-name schema)
                  (cond
@@ -142,30 +144,42 @@
 (rum/defc property-select
   [exclude-properties select-opts]
   (let [[properties set-properties!] (rum/use-state nil)
+        [classes set-classes!] (rum/use-state nil)
         [excluded-properties set-excluded-properties!] (rum/use-state nil)]
     (rum/use-effect!
      (fn []
-       (p/let [properties (db-async/<db-based-get-all-properties (state/get-current-repo))]
+       (p/let [repo (state/get-current-repo)
+               properties (db-async/<db-based-get-all-properties repo)
+               classes (->> (db-model/get-all-classes repo)
+                            (remove ldb/built-in?))]
+         (set-classes! classes)
          (set-properties! (remove exclude-properties properties))
          (set-excluded-properties! (->> properties
                                         (filter exclude-properties)
                                         (map :block/title)
                                         set))))
      [])
-    [:div.ls-property-add.flex.flex-row.items-center.property-key
-     [:div.ls-property-key
-      (select/select (merge
-                      {:items (map (fn [x]
-                                     {:label (:block/title x)
-                                      :value (:block/uuid x)}) properties)
-                       :extract-fn :label
-                       :dropdown? false
-                       :close-modal? false
-                       :new-case-sensitive? true
-                       :show-new-when-not-exact-match? true
-                       :exact-match-exclude-items (fn [s] (contains? excluded-properties s))
-                       :input-default-placeholder "Add or change property"}
-                      select-opts))]]))
+    (let [items (concat
+                 (map (fn [x]
+                        {:label (:block/title x)
+                         :value (:block/uuid x)}) properties)
+                 (map (fn [x]
+                        {:label (:block/title x)
+                         :value (:block/uuid x)
+                         :group "Tags"}) classes))]
+      [:div.ls-property-add.flex.flex-row.items-center.property-key
+       [:div.ls-property-key
+        (select/select (merge
+                        {:items items
+                         :grouped? true
+                         :extract-fn :label
+                         :dropdown? false
+                         :close-modal? false
+                         :new-case-sensitive? true
+                         :show-new-when-not-exact-match? true
+                         :exact-match-exclude-items (fn [s] (contains? excluded-properties s))
+                         :input-default-placeholder "Add or change property"}
+                        select-opts))]])))
 
 (rum/defc property-icon
   [property property-type]
@@ -194,7 +208,7 @@
   (fn [{:keys [value label]}]
     (reset! *property-key (if (uuid? value) label value))
     (let [property (when (uuid? value) (db/entity [:block/uuid value]))]
-      (when (and *show-new-property-config? (not property))
+      (when (and *show-new-property-config? (not (ldb/property? property)))
         (reset! *show-new-property-config? true))
       (reset! *property property)
       (when property
@@ -221,10 +235,26 @@
                  (not (seq (:property/closed-values property))))
             (pv/<create-new-block! block property "")
 
+            ;; using class as property
+            (and property (ldb/class? property))
+            (let [schema (assoc (:block/schema property)
+                                :type :node)]
+              (p/do!
+               (db/transact! (state/get-current-repo)
+                             [{:db/id (:db/id property)
+                               :db/ident (:db/ident property)
+                               :db/cardinality :db.cardinality/one
+                               :db/valueType :db.type/ref
+                               :db/index true
+                               :block/tags :logseq.class/Property
+                               :block/schema schema
+                               :property/schema.classes (:db/id property)}]
+                             {:outliner-op :save-block})
+               (reset! *show-new-property-config? false)))
+
             (or (not= :default type)
                 (and (= :default type) (seq (:property/closed-values property))))
-            (p/do!
-             (reset! *show-new-property-config? false))))))))
+            (reset! *show-new-property-config? false)))))))
 
 (rum/defc property-key-title
   [block property class-schema?]
@@ -333,17 +363,20 @@
         *show-new-property-config? (::show-new-property-config? state)
         *show-class-select? (::show-class-select? state)
         *property-schema (::property-schema state)
-        block-type (keyword (get block :block/type :block))
         page? (ldb/page? block)
+        block-types (let [types (ldb/get-entity-types block)]
+                      (cond-> types
+                        (and page? (not (contains? types :page)))
+                        (conj :page)
+                        (empty? types)
+                        #{:block}))
         exclude-properties (fn [m]
-                             (let [view-context (get-in m [:block/schema :view-context] :all)
-                                   block-types (if (and page? (not= block-type :page))
-                                                 #{:page block-type}
-                                                 #{block-type})]
+                             (let [view-context (get-in m [:block/schema :view-context] :all)]
                                (or (contains? #{:logseq.property/query} (:db/ident m))
                                    (and (not page?) (contains? #{:block/alias} (:db/ident m)))
                                    ;; Filters out properties from being in wrong :view-context and :never view-contexts
-                                   (and (not= view-context :all) (not (contains? block-types view-context))))))
+                                   (and (not= view-context :all) (not (contains? block-types view-context)))
+                                   (and (ldb/built-in? block) (contains? #{:logseq.property/parent} (:db/ident m))))))
         property (rum/react *property)
         property-key (rum/react *property-key)]
     [:div.ls-property-input.flex.flex-1.flex-row.items-center.flex-wrap.gap-1
@@ -392,7 +425,7 @@
 
 (rum/defcs new-property < rum/reactive
   [state block opts]
-  (when (and (not config/publishing?) (:class-schema? opts))
+  (when-not config/publishing?
     [:div.ls-new-property {:style {:margin-left 6 :margin-top 1}}
      [:a.fade-link.flex.jtrigger
       {:tab-index 0
@@ -553,7 +586,7 @@
    :will-remount (fn [state]
                    (let [block (db/entity (:db/id (::block state)))]
                      (assoc state ::classes (async-load-classes! block))))}
-  [state _target-block {:keys [class-schema?] :as opts}]
+  [state _target-block {:keys [class-schema? sidebar-properties?] :as opts}]
   (let [id (::id state)
         db-id (:db/id (::block state))
         block (db/sub-block db-id)
@@ -563,7 +596,6 @@
                                                      (and (set? ids) (contains? ids (:block/uuid block))))))
         _ (doseq [class (::classes state)]
             (db/sub-block (:db/id class)))
-        page? (db/page? block)
         class? (ldb/class? block)
         block-properties (:block/properties block)
         properties (cond
@@ -652,20 +684,38 @@
                                      (when (and class? (nil? (:logseq.property.class/properties block)))
                                        [[:logseq.property.class/properties nil]]))
                              remove-built-in-or-other-position-properties)]
-    (when-not (and (empty? full-properties) (not (:class-schema? opts)))
-      [:div.ls-properties-area
-       {:id id
-        :class (util/classnames [{:class-properties class-schema?
-                                  :ls-page-properties (and page? (not class-schema?))}])
-        :tab-index 0
-        :on-key-up #(when-let [block (and (= "Escape" (.-key %))
-                                          (.closest (.-target %) "[blockid]"))]
-                      (let [target (.-target %)]
-                        (when-not (d/has-class? target "ls-popup-closed")
-                          (state/set-selection-blocks! [block])
-                          (some-> js/document.activeElement (.blur)))
-                        (d/remove-class! target "ls-popup-closed")))}
-       (let [properties' (remove (fn [[k _v]] (contains? #{:logseq.property/icon :logseq.property/query} k)) full-properties)]
-         (properties-section block (if class-schema? properties properties') opts))
-
-       (rum/with-key (new-property block opts) (str id "-add-property"))])))
+    (cond
+      (and (empty? full-properties) (not (:class-schema? opts)))
+      (when sidebar-properties?
+        (rum/with-key (new-property block opts) (str id "-add-property")))
+
+      :else
+      (let [remove-properties #{:logseq.property/icon :logseq.property/query}
+            properties' (remove (fn [[k _v]] (contains? remove-properties k)) full-properties)
+            properties'' (cond->> properties'
+                           (not class-schema?)
+                           (remove (fn [[k _v]] (= k :logseq.property.class/properties))))
+            page? (ldb/page? block)]
+        [:div.ls-properties-area
+         {:id id
+          :class (util/classnames [{:class-properties class-schema?
+                                    :ls-page-properties (and page? (not class-schema?))}])
+          :tab-index 0
+          :on-key-up #(when-let [block (and (= "Escape" (.-key %))
+                                            (.closest (.-target %) "[blockid]"))]
+                        (let [target (.-target %)]
+                          (when-not (d/has-class? target "ls-popup-closed")
+                            (state/set-selection-blocks! [block])
+                            (some-> js/document.activeElement (.blur)))
+                          (d/remove-class! target "ls-popup-closed")))}
+         (properties-section block (if class-schema? properties properties'') opts)
+
+         (when page?
+           (rum/with-key (new-property block opts) (str id "-add-property")))
+
+         (when page?
+           (let [properties'' (filter (fn [[k _v]] (= k :logseq.property.class/properties)) properties')]
+             (when (seq properties'')
+               [:<>
+                (when-not class-schema? [:hr.my-4])
+                (properties-section block (if class-schema? properties properties'') opts)])))]))))

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

@@ -29,9 +29,7 @@
   margin-left: 0;
 }
 
-.ls-block .property-pair,
-.property-block .property-value,
-.block-property-value {
+.ls-block .property-pair, .ls-sidebar-page-properties .property-pair, .property-block .property-value, .block-property-value {
   margin-left: 7px;
 }
 

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

@@ -96,7 +96,7 @@
                (let [toggle-fn #(do
                                   (when (fn? on-hide) (on-hide))
                                   (shui/popup-hide! id))
-                     classes (model/get-all-classes (state/get-current-repo) {:except-root-class? true})
+                     classes (model/get-all-readable-classes (state/get-current-repo) {:except-root-class? true})
                      options (map (fn [class]
                                     {:label (:block/title class)
                                      :value (:block/uuid class)})

+ 22 - 11
src/main/frontend/components/property/value.cljs

@@ -5,7 +5,6 @@
             [dommy.core :as d]
             [frontend.components.icon :as icon-component]
             [frontend.components.select :as select]
-            [frontend.components.title :as title]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.db :as db]
@@ -34,7 +33,8 @@
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [clojure.set :as set]))
 
 (rum/defc property-empty-btn-value
   [property & opts]
@@ -506,13 +506,16 @@
                              (when (and property-type (not= property-type :node))
                                (if (= property-type :page)
                                  (not (db/page? node))
-                                 (not= property-type (some-> (:block/type node) keyword))))))
+                                 (not (contains? (ldb/get-entity-types node) property-type))))))
                        result)))))
 
         options (map (fn [node]
                        (let [id (or (:value node) (:db/id node))
                              [header label] (if (integer? id)
-                                              (let [title (subs (title/block-unique-title node) 0 256)
+                                              (let [node-title (if (seq (:property/schema.classes property))
+                                                                 (:block/title node)
+                                                                 (block-handler/block-unique-title node))
+                                                    title (subs node-title 0 256)
                                                     node (or (db/entity id) node)
                                                     icon (get-node-icon node)
                                                     header (when-not (db/page? node)
@@ -529,7 +532,10 @@
                                 :header header
                                 :label-value (:block/title node)
                                 :label label
-                                :value id))) nodes)
+                                :value id
+                                :disabled? (and tags? (contains?
+                                                       (set/union #{:logseq.class/Journal :logseq.class/Whiteboard} ldb/internal-tags)
+                                                       (:db/ident node)))))) nodes)
         classes' (remove (fn [class] (= :logseq.class/Root (:db/ident class))) classes)
         opts' (cond->
                (merge
@@ -547,7 +553,10 @@
                                               "Choose nodes"
                                               :else
                                               "Choose node")
-                 :show-new-when-not-exact-match? (if (and parent-property? (contains? (set children-pages) (:db/id block)))
+                 :show-new-when-not-exact-match? (if (or (and parent-property? (contains? (set children-pages) (:db/id block)))
+                                                         ;; Don't allow creating private tags
+                                                         (seq (set/intersection (set (map :db/ident classes))
+                                                                                ldb/private-tags)))
                                                    false
                                                    true)
                  :extract-chosen-fn :value
@@ -815,6 +824,9 @@
        (= value :logseq.property/empty-placeholder)
        (property-empty-btn-value property)
 
+       closed-values?
+       (closed-value-item value opts)
+
        (or (ldb/page? value)
            (and (seq (:block/tags value))
                 ;; FIXME: page-cp should be renamed to node-cp and
@@ -832,9 +844,6 @@
        (when-let [reference (state/get-component :block/reference)]
          (reference {} (:block/uuid value)))
 
-       closed-values?
-       (closed-value-item value opts)
-
        (de/entity? value)
        (when-some [content (str (db-property/property-value-content value))]
          (inline-text-cp content))
@@ -966,7 +975,9 @@
   (let [type (get schema :type :default)
         date? (= type :date)
         *el (rum/use-ref nil)
-        items (if (de/entity? v) #{v} v)]
+        items (cond->> (if (de/entity? v) #{v} v)
+                (= (:db/ident property) :block/tags)
+                (remove (fn [v] (contains? ldb/hidden-tags (:db/ident v)))))]
     (rum/use-effect!
      (fn []
        (when editing?
@@ -1002,7 +1013,7 @@
                            (do (some-> (rum/deref *el) (.click))
                                (util/stop e))
                            :dune))
-          :class "flex flex-1 flex-row items-center flex-wrap gap-x-2 gap-y-2 pr-4"}
+          :class "flex flex-1 flex-row items-center flex-wrap gap-x-2 gap-y-2"}
          (let [not-empty-value? (not= (map :db/ident items) [:logseq.property/empty-placeholder])]
            (if (and (seq items) not-empty-value?)
              (concat

+ 1 - 1
src/main/frontend/components/query/builder.cljs

@@ -227,7 +227,7 @@
         db-based? (config/db-based-graph? repo)]
     (rum/use-effect!
      (fn []
-       (let [result (db-model/get-all-classes repo {:except-root-class? true})]
+       (let [result (db-model/get-all-readable-classes repo {:except-root-class? true})]
          (set-values! result)))
      [])
     (let [items (->> values

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

@@ -307,8 +307,8 @@
         db-based? (config/db-based-graph? current-repo)
         repos (sort-repos-with-metadata-local repos)
         repos (distinct
-                (if (and (or (seq remotes) (seq rtc-graphs)) login?)
-                  (repo-handler/combine-local-&-remote-graphs repos (concat remotes rtc-graphs)) repos))
+               (if (and (or (seq remotes) (seq rtc-graphs)) login?)
+                 (repo-handler/combine-local-&-remote-graphs repos (concat remotes rtc-graphs)) repos))
         items-fn #(repos-dropdown-links repos current-repo downloading-graph-id opts)
         header-fn #(when (> (count repos) 1)                ; show switch to if there are multiple repos
                      [:div.font-medium.text-sm.opacity-50.px-1.py-1.flex.flex-row.justify-between.items-center
@@ -318,14 +318,14 @@
                         (if remotes-loading?
                           (ui/loading "")
                           (shui/button
-                            {:variant :ghost
-                             :size :sm
-                             :title "Refresh remote graphs"
-                             :class "!h-6 !px-1 relative right-[-4px]"
-                             :on-click (fn []
-                                         (file-sync/load-session-graphs)
-                                         (rtc-handler/<get-remote-graphs))}
-                            (ui/icon "refresh" {:size 15}))))])
+                           {:variant :ghost
+                            :size :sm
+                            :title "Refresh remote graphs"
+                            :class "!h-6 !px-1 relative right-[-4px]"
+                            :on-click (fn []
+                                        (file-sync/load-session-graphs)
+                                        (rtc-handler/<get-remote-graphs))}
+                           (ui/icon "refresh" {:size 15}))))])
         _remote? (and current-repo (:remote? (first (filter #(= current-repo (:url %)) repos))))
         _repo-name (when current-repo (db/get-repo-name current-repo))]
 
@@ -339,23 +339,23 @@
           (if hr
             (shui/dropdown-menu-separator)
             (shui/dropdown-menu-item
-              (assoc options
-                :title hover-detail
-                :on-click (fn [^js e]
-                            (when on-click'
-                              (when-not (false? (on-click' e))
-                                (shui/popup-hide! contentid)))))
-              (or item
-                (if href'
-                  [:a.flex.items-center.w-full
-                   {:href href' :on-click #(shui/popup-hide! contentid)
-                    :style {:color "inherit"}} title]
-                  [:span.flex.items-center.gap-1.w-full
-                   icon [:div title]]))))))]
+             (assoc options
+                    :title hover-detail
+                    :on-click (fn [^js e]
+                                (when on-click'
+                                  (when-not (false? (on-click' e))
+                                    (shui/popup-hide! contentid)))))
+             (or item
+                 (if href'
+                   [:a.flex.items-center.w-full
+                    {:href href' :on-click #(shui/popup-hide! contentid)
+                     :style {:color "inherit"}} title]
+                   [:span.flex.items-center.gap-1.w-full
+                    icon [:div title]]))))))]
      (repos-footer multiple-windows? db-based?)]))
 
 (rum/defcs repos-dropdown < rum/reactive
-                            (rum/local false ::electron-multiple-windows?)
+  (rum/local false ::electron-multiple-windows?)
   [state & {:as opts}]
   (let [current-repo (state/sub :git/current-repo)
         login? (boolean (state/sub :auth/id-token))]
@@ -365,38 +365,38 @@
           db-based? (config/db-based-graph? current-repo)
           repos (sort-repos-with-metadata-local repos)
           repos (distinct
-                  (if (and (or (seq remotes) (seq rtc-graphs)) login?)
-                    (repo-handler/combine-local-&-remote-graphs repos (concat remotes rtc-graphs)) repos))]
+                 (if (and (or (seq remotes) (seq rtc-graphs)) login?)
+                   (repo-handler/combine-local-&-remote-graphs repos (concat remotes rtc-graphs)) repos))]
       (let [remote? (and current-repo (:remote? (first (filter #(= current-repo (:url %)) repos))))
             repo-name (when current-repo (db/get-repo-name current-repo))
             short-repo-name (if current-repo
                               (db/get-short-repo-name repo-name)
                               "Select a Graph")]
         (shui/trigger-as :a
-          {:tab-index 0
-           :class "item cp__repos-select-trigger"
-           :on-pointer-down
-           (fn [^js e]
-             (check-multiple-windows? state)
-             (some-> (.-target e)
-               (.closest "a.item")
-               (shui/popup-show!
-                 (fn [{:keys [id]}] (repos-dropdown-content (assoc opts :contentid id)))
-                 {:as-dropdown? true
-                  :auto-focus? false
-                  :align "start"
-                  :content-props {:class "repos-list"
-                                  :data-mode (when db-based? "db")}})))
-           :title repo-name}      ;; show full path on hover
-          [:div.flex.relative.graph-icon.rounded
-           (shui/tabler-icon "database" {:size 15})]
-
-          [:div.repo-switch.pr-2.whitespace-nowrap
-           [:span.repo-name.font-medium
-            [:span.repo-text.overflow-hidden.text-ellipsis
-             (if (config/demo-graph? short-repo-name) "Demo" short-repo-name)]
-            (when remote? [:span.pl-1 (ui/icon "cloud")])]
-           [:span.dropdown-caret]])))))
+                         {:tab-index 0
+                          :class "item cp__repos-select-trigger"
+                          :on-pointer-down
+                          (fn [^js e]
+                            (check-multiple-windows? state)
+                            (some-> (.-target e)
+                                    (.closest "a.item")
+                                    (shui/popup-show!
+                                     (fn [{:keys [id]}] (repos-dropdown-content (assoc opts :contentid id)))
+                                     {:as-dropdown? true
+                                      :auto-focus? false
+                                      :align "start"
+                                      :content-props {:class "repos-list"
+                                                      :data-mode (when db-based? "db")}})))
+                          :title repo-name}      ;; show full path on hover
+                         [:div.flex.relative.graph-icon.rounded
+                          (shui/tabler-icon "database" {:size 15})]
+
+                         [:div.repo-switch.pr-2.whitespace-nowrap
+                          [:span.repo-name.font-medium
+                           [:span.repo-text.overflow-hidden.text-ellipsis
+                            (if (config/demo-graph? short-repo-name) "Demo" short-repo-name)]
+                           (when remote? [:span.pl-1 (ui/icon "cloud")])]
+                          [:span.dropdown-caret]])))))
 
 (rum/defcs graphs-selector < rum/reactive
   [_state]
@@ -413,10 +413,10 @@
      [:a.item.flex.items-center.gap-1.select-none
       {:on-click (fn [^js e]
                    (shui/popup-show! (.closest (.-target e) "a")
-                     (fn [{:keys [id]}] (repos-dropdown-content {:contentid id}))
-                     {:as-dropdown? true
-                      :content-props {:class "repos-list"}
-                      :align :start}))}
+                                     (fn [{:keys [id]}] (repos-dropdown-content {:contentid id}))
+                                     {:as-dropdown? true
+                                      :content-props {:class "repos-list"}
+                                      :align :start}))}
       [:span.thumb (shui/tabler-icon (if remote? "cloud" (if db-based? "database" "folder")) {:size 16})]
       [:strong short-repo-name]
       (shui/tabler-icon "selector" {:size 18})]]))

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

@@ -97,8 +97,8 @@
     (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])
           page (db/entity repo lookup)]
       (if (ldb/page? page)
-        [[:.flex.items-center.page-title
-          (icon/get-node-icon-cp page {:class "text-md mr-2"})
+        [[:.flex.items-center.page-title.gap-1
+          (icon/get-node-icon-cp page {:class "text-md"})
           [:span.overflow-hidden.text-ellipsis (:block/title page)]]
          (page-cp repo (str (:block/uuid page)))]
         (block-with-breadcrumb repo page idx [repo db-id block-type] false)))

+ 0 - 4
src/main/frontend/components/right_sidebar.css

@@ -38,7 +38,3 @@ html[data-theme=light] {
     @apply opacity-100;
   }
 }
-
-.sidebar-panel-content {
-  @apply pt-3;
-}

+ 5 - 3
src/main/frontend/components/select.cljs

@@ -29,7 +29,8 @@
               (when multiple-choices?
                 (ui/checkbox {:checked (boolean (selected-choices (:value result)))
                               :on-click (fn [e]
-                                          (.preventDefault e))}))
+                                          (.preventDefault e))
+                              :disabled (:disabled? result)}))
               value]
              (when (and (map? result) (:id result))
                [:div.tip.flex
@@ -67,7 +68,7 @@
    :will-unmount (fn [state]
                    (shui/dialog-close! :ls-select-modal)
                    state)}
-  [state {:keys [items limit on-chosen empty-placeholder
+  [state {:keys [items limit on-chosen empty-placeholder grouped?
                  prompt-key input-default-placeholder close-modal?
                  extract-fn extract-chosen-fn host-opts on-input input-opts
                  item-cp transform-fn tap-*input-val
@@ -138,7 +139,8 @@
                            [:div.item-results-wrap
                             (ui/auto-complete
                              search-result
-                             {:item-render       (or item-cp (fn [result chosen?]
+                             {:grouped? grouped?
+                              :item-render       (or item-cp (fn [result chosen?]
                                                                (render-item result chosen? multiple-choices? *selected-choices)))
                               :class             "cp__select-results"
                               :on-chosen         (fn [raw-chosen e]

+ 0 - 30
src/main/frontend/components/title.cljs

@@ -1,30 +0,0 @@
-(ns frontend.components.title
-  (:require [clojure.string :as string]
-            [frontend.db :as db]
-            [logseq.db :as ldb]
-            [datascript.impl.entity :as de]))
-
-(defn block-unique-title
-  "Multiple pages/objects may have the same `:block/title`.
-   Notice: this doesn't prevent for pages/objects that have the same tag or created by different clients."
-  [block]
-  (let [block-e (cond
-                  (de/entity? block)
-                  block
-                  (uuid? (:block/uuid block))
-                  (db/entity [:block/uuid (:block/uuid block)])
-                  :else
-                  block)
-        tags (remove (fn [t] (some-> (:block/raw-title block-e) (ldb/inline-tag? t)))
-                     (map (fn [tag] (if (number? tag) (db/entity tag) tag)) (:block/tags block)))]
-    (if (and (seq tags)
-             (not (ldb/journal? block)))
-      (str (:block/title block)
-           " "
-           (string/join
-            ", "
-            (keep (fn [tag]
-                    (when-let [title (:block/title tag)]
-                      (str "#" title)))
-                  tags)))
-      (:block/title block))))

+ 74 - 79
src/main/frontend/components/views.cljs

@@ -80,11 +80,11 @@
      {:variant "text"
       :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
       :on-click #(column-toggle-sorting! column)}
-      (let [title (str (:name column))]
-        [:span {:title title
-                :class "max-w-full overflow-hidden text-ellipsis"}
-         title])
-      (case asc?
+     (let [title (str (:name column))]
+       [:span {:title title
+               :class "max-w-full overflow-hidden text-ellipsis"}
+        title])
+     (case asc?
        true
        (ui/icon "arrow-up")
        false
@@ -529,7 +529,6 @@
 (defn- get-property-values
   [rows property]
   (let [property-ident (:db/ident property)
-        block-type? (= property-ident :block/type)
         values (->> (mapcat (fn [e] (let [e' (db/entity (:db/id e))
                                           v (get e' property-ident)]
                                       (if (set? v) v #{v}))) rows)
@@ -537,9 +536,8 @@
                     (distinct))]
     (->>
      (map (fn [e]
-            (let [label (get-property-value-content e)
-                  label' (if (and block-type? (= label "class")) "tag" label)]
-              {:label (str label') :value e}))
+            (let [label (get-property-value-content e)]
+              {:label (str label) :value e}))
           values)
      (sort-by :label))))
 
@@ -607,7 +605,7 @@
                                  (do
                                    (shui/popup-hide!)
                                    (let [property internal-property
-                                         new-filter [(:db/ident property) (if (= (:db/ident property) :block/type) :is :text-contains)]
+                                         new-filter [(:db/ident property) :text-contains]
                                          filters' (if (seq filters)
                                                     (conj filters new-filter)
                                                     [new-filter])]
@@ -685,15 +683,14 @@
     [:before :after]
     (concat
      [:is :is-not]
-     (when-not (= :block/type (:db/ident property))
-       (case (get-in property [:block/schema :type])
-         (:default :url :node)
-         [:text-contains :text-not-contains]
-         (:date)
-         [:date-before :date-after]
-         :number
-         [:number-gt :number-lt :number-gte :number-lte :between]
-         nil)))))
+     (case (get-in property [:block/schema :type])
+       (:default :url :node)
+       [:text-contains :text-not-contains]
+       (:date)
+       [:date-before :date-after]
+       :number
+       [:number-gt :number-lt :number-gte :number-lte :between]
+       nil))))
 
 (defn- get-filter-with-changed-operator
   [_property operator value]
@@ -821,16 +818,11 @@
        {:class "!px-2 rounded-none border-r"
         :variant "ghost"
         :size :sm}
-       (let [block-type? (= (:db/ident property) :block/type)
-             value (cond
+       (let [value (cond
                      (uuid? value)
                      (db/entity [:block/uuid value])
                      (and (coll? value) (every? uuid? value))
                      (set (map #(db/entity [:block/uuid %]) value))
-                     (and block-type? (coll? value))
-                     (map (fn [v] (if (= v "class") "tag" v)) value)
-                     (and block-type? (= value "class"))
-                     "tag"
                      :else
                      value)]
          [:div.flex.flex-row.items-center.gap-1.text-xs
@@ -1012,15 +1004,15 @@
 (rum/defc new-record-button < rum/static
   [table view-entity]
   (let [asset? (and (:logseq.property/built-in? view-entity)
-                 (= (:block/name view-entity) "asset"))]
+                    (= (:block/name view-entity) "asset"))]
     (ui/tooltip
-      (shui/button
-        {:variant "ghost"
-         :class "!px-1 text-muted-foreground"
-         :size :sm
-         :on-click (get-in table [:data-fns :add-new-object!])}
-        (ui/icon (if asset? "upload" "plus")))
-      [:div "New record"])))
+     (shui/button
+      {:variant "ghost"
+       :class "!px-1 text-muted-foreground"
+       :size :sm
+       :on-click (get-in table [:data-fns :add-new-object!])}
+      (ui/icon (if asset? "upload" "plus")))
+     [:div "New record"])))
 
 (rum/defc add-new-row < rum/static
   [table]
@@ -1084,8 +1076,8 @@
         *rows-wrap (rum/use-ref nil)]
 
     (rum/use-effect!
-      (fn [] (set-ready? true))
-      [])
+     (fn [] (set-ready? true))
+     [])
 
     (shui/table
      (let [columns' (:columns table)
@@ -1097,17 +1089,17 @@
            (table-header table columns' option selected-rows)
 
            (ui/virtualized-list
-             {:ref #(reset! *scroller-ref %)
-              :custom-scroll-parent (or (some-> (rum/deref *rows-wrap) (.closest ".sidebar-item-list"))
-                                      (gdom/getElement "main-content-container"))
-              :increase-viewport-by {:top 300 :bottom 300}
-              :compute-item-key (fn [idx]
-                                  (let [block (nth rows idx)]
-                                    (str "table-row-" (:db/id block))))
-              :total-count (count rows)
-              :item-content (fn [idx]
-                              (let [row (nth rows idx)]
-                                (table-row table row columns' {} option)))})
+            {:ref #(reset! *scroller-ref %)
+             :custom-scroll-parent (or (some-> (rum/deref *rows-wrap) (.closest ".sidebar-item-list"))
+                                       (gdom/getElement "main-content-container"))
+             :increase-viewport-by {:top 300 :bottom 300}
+             :compute-item-key (fn [idx]
+                                 (let [block (nth rows idx)]
+                                   (str "table-row-" (:db/id block))))
+             :total-count (count rows)
+             :item-content (fn [idx]
+                             (let [row (nth rows idx)]
+                               (table-row table row columns' {} option)))})
 
            (when add-new-object!
              (shui/table-footer (add-new-row table)))])]))))
@@ -1245,39 +1237,42 @@
 
     [:div.flex.flex-col.gap-2.grid
      {:ref *view-ref}
-     [:div.flex.flex-wrap.items-center.justify-between.gap-1
-      (when-not render-empty-title?
-        [:div.flex.flex-row.items-center.gap-2
-         (or
-          views-title
-          [:div.font-medium.opacity-50.text-sm
-           (t (or title-key :views.table/default-title)
-              (count (:rows table)))])])
-      [:div.view-actions.flex.items-center.gap-1
-
-       (filter-properties columns table)
-
-       (search input {:on-change set-input!
-                      :set-input! set-input!})
-
-       [:div.text-muted-foreground.text-sm
-        (pv/property-value view-entity (db/entity :logseq.property.view/type)
-                           (db/entity display-type) {})]
-
-       (more-actions columns table)
-
-       (when add-new-object! (new-record-button table view-entity))]]
-
-     (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))]))
+     (ui/foldable
+      [:div.flex.flex-1.flex-wrap.items-center.justify-between.gap-1
+       (when-not render-empty-title?
+         [:div.flex.flex-row.items-center.gap-2
+          (or
+           views-title
+           [:div.font-medium.opacity-50.text-sm
+            (t (or title-key :views.table/default-title)
+               (count (:rows table)))])])
+       [:div.view-actions.flex.items-center.gap-1
+
+        (filter-properties columns table)
+
+        (search input {:on-change set-input!
+                       :set-input! set-input!})
+
+        [:div.text-muted-foreground.text-sm
+         (pv/property-value view-entity (db/entity :logseq.property.view/type)
+                            (db/entity display-type) {})]
+
+        (more-actions columns table)
+
+        (when add-new-object! (new-record-button table view-entity))]]
+      (fn []
+        [: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))])
+      {:title-trigger? false})]))
 
 (rum/defcs view
   "Provides a view for data like query results and tagged objects, multiple

+ 4 - 4
src/main/frontend/context/i18n.cljs

@@ -31,10 +31,10 @@
 (defn tt
   [& keys]
   (some->
-    (medley/find-first
-      #(not (string/starts-with? (t %) "{Missing key"))
-      keys)
-    t))
+   (medley/find-first
+    #(not (string/starts-with? (t %) "{Missing key"))
+    keys)
+   t))
 
 (defn- fetch-local-language []
   (.. js/window -navigator -language))

+ 1 - 1
src/main/frontend/db/async.cljs

@@ -55,7 +55,7 @@
   [graph & {:keys [remove-built-in-property? remove-non-queryable-built-in-property?]
             :or {remove-built-in-property? true
                  remove-non-queryable-built-in-property? false}}]
-  (let [result (->> (d/datoms (db/get-db graph) :avet :block/type "property")
+  (let [result (->> (d/datoms (db/get-db graph) :avet :block/tags :logseq.class/Property)
                     (map (fn [datom] (db/entity (:e datom))))
                     (sort-by (juxt ldb/built-in? :block/title)))]
     (cond->> result

+ 92 - 45
src/main/frontend/db/model.cljs

@@ -383,10 +383,10 @@ independent of format as format specific heading characters are stripped"
 
 (defn page-exists?
   "Whether a page exists."
-  [page-name type]
+  [page-name tags]
   (let [repo (state/get-current-repo)]
     (when-let [db (conn/get-db repo)]
-      (ldb/page-exists? db page-name type))))
+      (ldb/page-exists? db page-name tags))))
 
 (defn page-empty?
   "Whether a page is empty. Does it has a non-page block?
@@ -531,14 +531,23 @@ independent of format as format specific heading characters are stripped"
 (defn get-journals-length
   []
   (let [today (date-time-util/date->int (js/Date.))]
-    (d/q '[:find (count ?page) .
-           :in $ ?today
-           :where
-           [?page :block/type "journal"]
-           [?page :block/journal-day ?journal-day]
-           [(<= ?journal-day ?today)]]
-         (conn/get-db (state/get-current-repo))
-         today)))
+    (if (config/db-based-graph?)
+      (d/q '[:find (count ?page) .
+             :in $ ?today
+             :where
+             [?page :block/tags :logseq.class/Journal]
+             [?page :block/journal-day ?journal-day]
+             [(<= ?journal-day ?today)]]
+           (conn/get-db (state/get-current-repo))
+           today)
+      (d/q '[:find (count ?page) .
+             :in $ ?today
+             :where
+             [?page :block/type "journal"]
+             [?page :block/journal-day ?journal-day]
+             [(<= ?journal-day ?today)]]
+           (conn/get-db (state/get-current-repo))
+           today))))
 
 (defn get-latest-journals
   ([n]
@@ -749,17 +758,29 @@ independent of format as format specific heading characters are stripped"
 
 (defn get-all-whiteboards
   [repo]
-  (d/q
-   '[:find [(pull ?page [:db/id
-                         :block/uuid
-                         :block/name
-                         :block/title
-                         :block/created-at
-                         :block/updated-at]) ...]
-     :where
-     [?page :block/name]
-     [?page :block/type "whiteboard"]]
-   (conn/get-db repo)))
+  (if (config/db-based-graph?)
+    (d/q
+     '[:find [(pull ?page [:db/id
+                           :block/uuid
+                           :block/name
+                           :block/title
+                           :block/created-at
+                           :block/updated-at]) ...]
+       :where
+       [?page :block/name]
+       [?page :block/tags :logseq.class/Whiteboard]]
+     (conn/get-db repo))
+    (d/q
+     '[:find [(pull ?page [:db/id
+                           :block/uuid
+                           :block/name
+                           :block/title
+                           :block/created-at
+                           :block/updated-at]) ...]
+       :where
+       [?page :block/name]
+       [?page :block/type "whiteboard"]]
+     (conn/get-db repo))))
 
 (defn get-whiteboard-id-nonces
   [repo page-id]
@@ -777,16 +798,27 @@ independent of format as format specific heading characters are stripped"
                     :nonce (:nonce shape)}))))))
 
 (defn get-all-classes
-  [repo & {:keys [except-root-class?]
-           :or {except-root-class? false}}]
+  [repo & {:keys [except-root-class? except-private-tags?]
+           :or {except-root-class? false
+                except-private-tags? true}}]
   (let [db (conn/get-db repo)
-        classes (->> (d/datoms db :avet :block/type "class")
+        classes (->> (d/datoms db :avet :block/tags :logseq.class/Tag)
                      (map (fn [d]
-                            (db-utils/entity db (:e d)))))]
+                            (db-utils/entity db (:e d))))
+                     (remove (fn [d]
+                               (and except-private-tags?
+                                    (contains? ldb/private-tags (:db/ident d))))))]
     (if except-root-class?
       (keep (fn [e] (when-not (= :logseq.class/Root (:db/ident e)) e)) classes)
       classes)))
 
+(defn get-all-readable-classes
+  "Gets all classes that are used in a read only context e.g. querying or used
+  for property value selection. This should _not_ be used in a write context e.g.
+  adding a tag to a node or creating a new node with a tag"
+  [repo opts]
+  (get-all-classes repo (merge opts {:except-private-tags? false})))
+
 (defn get-structured-children
   [repo eid]
   (->>
@@ -802,13 +834,15 @@ independent of format as format specific heading characters are stripped"
 (defn get-class-objects
   [repo class-id]
   (when-let [class (db-utils/entity repo class-id)]
-    (if (first (:logseq.property/_parent class))        ; has children classes
-      (let [all-classes (conj (->> (get-structured-children repo class-id)
-                                   (map #(db-utils/entity repo %)))
-                              class)]
-        (->> (mapcat :block/_tags all-classes)
-             distinct))
-      (:block/_tags class))))
+    (->>
+     (if (first (:logseq.property/_parent class))        ; has children classes
+       (let [all-classes (conj (->> (get-structured-children repo class-id)
+                                    (map #(db-utils/entity repo %)))
+                               class)]
+         (->> (mapcat :block/_tags all-classes)
+              distinct))
+       (:block/_tags class))
+     (remove ldb/hidden?))))
 
 (defn sub-class-objects
   [repo class-id]
@@ -829,7 +863,8 @@ independent of format as format specific heading characters are stripped"
               (rules/extract-rules rules/db-query-dsl-rules [:has-property-or-default-value]
                                    {:deps rules/rules-dependencies})
               (:db/ident property))
-         (map #(db-utils/entity repo %)))))
+         (map #(db-utils/entity repo %))
+         (remove ldb/hidden?))))
 
 (defn get-all-namespace-relation
   [repo]
@@ -849,18 +884,30 @@ independent of format as format specific heading characters are stripped"
 (defn get-pages-relation
   [repo with-journal?]
   (when-let [db (conn/get-db repo)]
-    (let [q (if with-journal?
-              '[:find ?p ?ref-page
-                :where
-                [?block :block/page ?p]
-                [?block :block/refs ?ref-page]]
-              '[:find ?p ?ref-page
-                :where
-                [?block :block/page ?p]
-                [(get-else $ ?p :block/type "N/A") ?type]
-                [(not= ?type "journal")]
-                [?block :block/refs ?ref-page]])]
-      (d/q q db))))
+    (if (config/db-based-graph?)
+      (let [q (if with-journal?
+                '[:find ?p ?ref-page
+                  :where
+                  [?block :block/page ?p]
+                  [?block :block/refs ?ref-page]]
+                '[:find ?p ?ref-page
+                  :where
+                  [?block :block/page ?p]
+                  [?p :block/tags]
+                  (not [?p :block/tags :logseq.class/Journal])
+                  [?block :block/refs ?ref-page]])]
+        (d/q q db))
+      (let [q (if with-journal?
+                '[:find ?p ?ref-page
+                  :where
+                  [?block :block/page ?p]
+                  [?block :block/refs ?ref-page]]
+                '[:find ?p ?ref-page
+                  :where
+                  [?block :block/page ?p]
+                  (not [?p :block/type "journal"])
+                  [?block :block/refs ?ref-page]])]
+        (d/q q db)))))
 
 (defn get-namespace-pages
   "Accepts both sanitized and unsanitized namespaces"

+ 1 - 1
src/main/frontend/db/query_dsl.cljs

@@ -323,7 +323,7 @@
     (or (some->> (name property-name)
                  (db-utils/q '[:find [(pull ?b [:db/ident]) ...]
                                :in $ ?title
-                               :where [?b :block/type "property"] [?b :block/title ?title]])
+                               :where [?b :block/tags :logseq.class/Property] [?b :block/title ?title]])
                  first
                  :db/ident)
         ;; Don't return nil as that incorrectly matches all properties

+ 28 - 1
src/main/frontend/handler/block.cljs

@@ -22,7 +22,8 @@
    [frontend.handler.property.util :as pu]
    [dommy.core :as dom]
    [goog.object :as gobj]
-   [promesa.core :as p]))
+   [promesa.core :as p]
+   [datascript.impl.entity :as de]))
 
 ;;  Fns
 
@@ -188,6 +189,32 @@
     (-> (property-util/remove-built-in-properties format content)
         (drawer/remove-logbook))))
 
+(defn block-unique-title
+  "Multiple pages/objects may have the same `:block/title`.
+   Notice: this doesn't prevent for pages/objects that have the same tag or created by different clients."
+  [block]
+  (let [block-e (cond
+                  (de/entity? block)
+                  block
+                  (uuid? (:block/uuid block))
+                  (db/entity [:block/uuid (:block/uuid block)])
+                  :else
+                  block)
+        tags (remove (fn [t]
+                       (or (some-> (:block/raw-title block-e) (ldb/inline-tag? t))
+                           (ldb/private-tags (:db/ident t))))
+                     (map (fn [tag] (if (number? tag) (db/entity tag) tag)) (:block/tags block)))]
+    (if (seq tags)
+      (str (:block/title block)
+           " "
+           (string/join
+            ", "
+            (keep (fn [tag]
+                    (when-let [title (:block/title tag)]
+                      (str "#" title)))
+                  tags)))
+      (:block/title block))))
+
 (defn edit-block!
   [block pos & {:keys [_container-id custom-content tail-len save-code-editor?]
                 :or {tail-len 0

+ 15 - 18
src/main/frontend/handler/db_based/page.cljs

@@ -4,15 +4,16 @@
             [frontend.db :as db]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.common.page :as page-common-handler]
+            [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.notification :as notification]
             [frontend.state :as state]
-            [frontend.modules.outliner.ui :as ui-outliner-tx]
             [logseq.outliner.validate :as outliner-validate]
             [logseq.db.frontend.class :as db-class]
             [logseq.common.util :as common-util]
             [logseq.common.util.page-ref :as page-ref]
             [datascript.impl.entity :as de]
-            [promesa.core :as p]))
+            [promesa.core :as p]
+            [logseq.db]))
 
 (defn- valid-tag?
   "Returns a boolean indicating whether the new tag passes all valid checks.
@@ -32,27 +33,23 @@
         (throw e)))))
 
 (defn add-tag [repo block-id tag-entity]
-  (ui-outliner-tx/transact!
-   {:outliner-op :save-block}
-   (p/do!
-    (editor-handler/save-current-block!)
-    ;; Check after save-current-block to get most up to date block content
-    (when (valid-tag? repo (db/entity repo [:block/uuid block-id]) tag-entity)
-      (let [tx-data [[:db/add [:block/uuid block-id] :block/tags (:db/id tag-entity)]
-                     ;; TODO: Move this to outliner.core to consistently add refs for tags
-                     [:db/add [:block/uuid block-id] :block/refs (:db/id tag-entity)]]]
-        (db/transact! repo tx-data {:outliner-op :save-block}))))))
+  (p/do!
+   (editor-handler/save-current-block!)
+   ;; Check after save-current-block to get most up to date block content
+   (when (valid-tag? repo (db/entity repo [:block/uuid block-id]) tag-entity)
+     (db-property-handler/set-block-property! block-id :block/tags (:db/id tag-entity)))))
 
 (defn convert-to-tag!
   [page-entity]
-  (if (db/page-exists? (:block/title page-entity) "class")
+  (if (db/page-exists? (:block/title page-entity) #{:logseq.class/Tag})
     (notification/show! (str "A tag with the name \"" (:block/title page-entity) "\" already exists.") :warning false)
-    (let [class (db-class/build-new-class (db/get-db)
-                                          {:db/id (:db/id page-entity)
-                                           :block/title (:block/title page-entity)
-                                           :block/created-at (:block/created-at page-entity)})]
+    (let [txs [(db-class/build-new-class (db/get-db)
+                                         {:db/id (:db/id page-entity)
+                                          :block/title (:block/title page-entity)
+                                          :block/created-at (:block/created-at page-entity)})
+               [:db/retract (:db/id page-entity) :block/tags :logseq.class/Page]]]
 
-      (db/transact! (state/get-current-repo) [class] {:outliner-op :save-block}))))
+      (db/transact! (state/get-current-repo) txs {:outliner-op :save-block}))))
 
 (defn <create-class!
   "Creates a class page and provides class-specific error handling"

+ 13 - 14
src/main/frontend/handler/graph.cljs

@@ -11,8 +11,7 @@
             [frontend.storage :as storage]
             [logseq.graph-parser.db :as gp-db]
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
-            [logseq.db :as ldb]
-            [frontend.components.title :as title]))
+            [logseq.db :as ldb]))
 
 (defn- build-links
   [links]
@@ -45,7 +44,7 @@
                    size (int (* 8 (max 1.0 (js/Math.cbrt n))))]
                (cond->
                 {:id (str (:db/id p))
-                 :label (title/block-unique-title p)
+                 :label page-title
                  :size size
                  :color color
                  :block/created-at (:block/created-at p)}
@@ -72,8 +71,8 @@
 (defn- normalize-page-name
   [{:keys [nodes links]}]
   (let [nodes' (->> (remove-uuids-and-files! nodes)
-                   (util/distinct-by (fn [node] (:id node)))
-                   (remove nil?))]
+                    (util/distinct-by (fn [node] (:id node)))
+                    (remove nil?))]
     {:nodes nodes'
      :links links}))
 
@@ -207,15 +206,15 @@
   (let [search-nodes (fn [forward?]
                        (let [links (group-by (if forward? :source :target) links)]
                          (loop [nodes nodes
-                               level level]
-                          (if (zero? level)
-                            nodes
-                            (recur (distinct (apply concat nodes
-                                               (map
-                                                 (fn [id]
-                                                   (->> (get links id) (map (if forward? :target :source))))
-                                                 nodes)))
-                                   (dec level))))))
+                                level level]
+                           (if (zero? level)
+                             nodes
+                             (recur (distinct (apply concat nodes
+                                                     (map
+                                                      (fn [id]
+                                                        (->> (get links id) (map (if forward? :target :source))))
+                                                      nodes)))
+                                    (dec level))))))
         nodes (concat (search-nodes true) (search-nodes false))
         nodes (set nodes)]
     (update graph :nodes

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

@@ -15,7 +15,7 @@
   (when (and page (state/enable-journals? (state/get-current-repo)))
     (p/do!
      (db-async/<get-block (state/get-current-repo) page :children? false)
-     (if (db-model/page-exists? page "journal")
+     (if (db-model/page-exists? page #{:logseq.class/Journal})
        (route-handler/redirect! {:to          :page
                                  :path-params {:name page}})
        (page-handler/<create! page)))))

+ 0 - 1
src/main/frontend/handler/page.cljs

@@ -425,7 +425,6 @@
                           (<create! title {:redirect? false
                                            :split-namespace? false
                                            :create-first-block? (not template)
-                                           :journal? true
                                            :today-journal? true})
                           (state/pub-event! [:journal/insert-template today-page])
                           (ui-handler/re-render-root!)

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

@@ -69,7 +69,7 @@
     {:db/id (:db/id page-entity)
      :block/title page-name
      :block/name (util/page-name-sanity-lc page-name)
-     :block/type "whiteboard"
+     :block/tags :logseq.class/Whiteboard
      :block/format :markdown
      :logseq.property/ls-type :whiteboard-page
      :logseq.property.tldraw/page tldraw-page

+ 2 - 1
src/main/frontend/modules/outliner/ui.cljc

@@ -4,7 +4,8 @@
                      [frontend.db.transact]
                      [frontend.db.conn]
                      [logseq.outliner.op]
-                     [frontend.modules.outliner.op])))
+                     [frontend.modules.outliner.op]
+                     [logseq.db])))
 
 (defmacro transact!
   [opts & body]

+ 52 - 42
src/main/frontend/ui.cljs

@@ -511,42 +511,53 @@
            empty-placeholder
            item-render
            class
-           header]}]
+           header
+           grouped?]}]
   (let [*current-idx (get state ::current-idx)
-        *groups (atom #{})]
+        *groups (atom #{})
+        render-f (fn [matched]
+                   (for [[idx item] (medley/indexed matched)]
+                     (let [react-key (str idx)
+                           item-cp
+                           [:div.menu-link-wrap
+                            {:key react-key
+                   ;; mouse-move event to indicate that cursor moved by user
+                             :on-mouse-move  #(reset! *current-idx idx)}
+                            (let [chosen? (= @*current-idx idx)]
+                              (menu-link
+                               {:id (str "ac-" react-key)
+                                :tab-index "0"
+                                :class (when chosen? "chosen")
+                       ;; TODO: should have more tests on touch devices
+                       ;:on-pointer-down #(util/stop %)
+                                :on-click (fn [e]
+                                            (util/stop e)
+                                            (when-not (:disabled? item)
+                                              (if (and (gobj/get e "shiftKey") on-shift-chosen)
+                                                (on-shift-chosen item)
+                                                (on-chosen item e))))}
+                               (if item-render (item-render item chosen?) item)))]]
+
+                       (let [group-name (and (fn? get-group-name) (get-group-name item))]
+                         (if (and group-name (not (contains? @*groups group-name)))
+                           (do
+                             (swap! *groups conj group-name)
+                             [:div
+                              [:div.ui__ac-group-name group-name]
+                              item-cp])
+                           item-cp)))))]
     [:div#ui__ac {:class class}
      (if (seq matched)
        [:div#ui__ac-inner.hide-scrollbar
         (when header header)
-        (for [[idx item] (medley/indexed matched)]
-          (let [react-key (str idx)
-                item-cp
-                [:div.menu-link-wrap
-                 {:key react-key
-                   ;; mouse-move event to indicate that cursor moved by user
-                  :on-mouse-move  #(reset! *current-idx idx)}
-                 (let [chosen? (= @*current-idx idx)]
-                   (menu-link
-                    {:id (str "ac-" react-key)
-                     :tab-index "0"
-                     :class (when chosen? "chosen")
-                       ;; TODO: should have more tests on touch devices
-                       ;:on-pointer-down #(util/stop %)
-                     :on-click (fn [e]
-                                 (util/stop e)
-                                 (if (and (gobj/get e "shiftKey") on-shift-chosen)
-                                   (on-shift-chosen item)
-                                   (on-chosen item e)))}
-                    (if item-render (item-render item chosen?) item)))]]
-
-            (let [group-name (and (fn? get-group-name) (get-group-name item))]
-              (if (and group-name (not (contains? @*groups group-name)))
-                (do
-                  (swap! *groups conj group-name)
-                  [:div
-                   [:div.ui__ac-group-name group-name]
-                   item-cp])
-                item-cp))))]
+        (if grouped?
+          (for [[group matched] (group-by :group matched)]
+            (if group
+              [:div
+               [:div.ui__ac-group-name group]
+               (render-f matched)]
+              (render-f matched)))
+          (render-f matched))]
        (when empty-placeholder
          empty-placeholder))]))
 
@@ -593,28 +604,27 @@
   (rum/local false ::control?)
   [state {:keys [on-pointer-down header title-trigger? collapsed?]}]
   (let [control? (get state ::control?)]
-    [:div.content
+    [:div.ls-foldable-title.content
      [:div.flex-1.flex-row.foldable-title (cond->
                                            {:on-mouse-over #(reset! control? true)
                                             :on-mouse-out  #(reset! control? false)}
                                             title-trigger?
                                             (assoc :on-pointer-down on-pointer-down
                                                    :class "cursor"))
-      [:div.flex.flex-row.items-center.ls-foldable-header
+      [:div.flex.flex-row.items-center.ls-foldable-header.gap-1
        {:on-click (fn [^js e]
                     (let [^js target (.-target e)]
                       (when (some-> target (.closest ".as-toggle"))
                         (reset! collapsed? (not @collapsed?)))))}
        (when-not (mobile-util/native-platform?)
-         [:a.block-control.opacity-50.hover:opacity-100.mr-2
-          (cond->
-           {:style    {:width       14
-                       :height      16
-                       :margin-left -30}}
-            (not title-trigger?)
-            (assoc :on-pointer-down on-pointer-down))
-          [:span {:class (if (or @control? @collapsed?) "control-show cursor-pointer" "control-hide")}
-           (rotating-arrow @collapsed?)]])
+         (let [style {:width 14 :height 16}]
+           [:a.ls-foldable-title-control.block-control.opacity-50.hover:opacity-100
+            (cond->
+             {:style style}
+              (not title-trigger?)
+              (assoc :on-pointer-down on-pointer-down))
+            [:span {:class (if (or @control? @collapsed?) "control-show cursor-pointer" "control-hide")}
+             (rotating-arrow @collapsed?)]]))
        (if (fn? header)
          (header @collapsed?)
          header)]]]))

+ 5 - 1
src/main/frontend/ui.css

@@ -329,4 +329,8 @@ input[type='range'] {
   .as-toggle {
     @apply opacity-60 cursor-pointer select-none active:opacity-50;
   }
-}
+}
+
+.ls-foldable-title-control {
+  margin-left: -27px;
+}

+ 64 - 31
src/main/frontend/worker/db/migrate.cljs

@@ -417,6 +417,34 @@
                 :block/title title'})))))
      datoms)))
 
+(defn- replace-block-type-with-tags
+  [conn _search-db]
+  (let [db @conn
+        block-type-entity (d/entity db :block/type)
+        ;; Not using (d/datoms db :avet :block/type) here because some old graphs
+        ;; don't have :block/type indexed
+        datoms (->> (d/datoms db :eavt)
+                    (filter (fn [d] (= :block/type (:a d)))))
+        journal-entity (d/entity db :logseq.class/Journal)
+        tx-data (mapcat (fn [{:keys [e _a v]}]
+                          (let [tag (case v
+                                      "page" :logseq.class/Page
+                                      "class" :logseq.class/Tag
+                                      "property" :logseq.class/Property
+                                      "journal" :logseq.class/Journal
+                                      "whiteboard" :logseq.class/Whiteboard
+                                      "closed value" nil
+                                      (throw (ex-info "unsupported block/type" {:type v})))]
+                            (cond->
+                             [[:db/retract e :block/type]]
+                              (some? tag)
+                              (conj [:db/add e :block/tags tag])))) datoms)]
+    (concat
+     ;; set journal's tag to `#Page`
+     [[:db/add (:db/id journal-entity) :block/tags :logseq.class/Page]]
+     tx-data
+     [[:db/retractEntity (:db/id block-type-entity)]])))
+
 (defn- deprecate-logseq-user-ns
   [conn _search-db]
   (let [db @conn]
@@ -514,9 +542,10 @@
    [47 {:fix replace-hidden-type-with-schema}]
    [48 {:properties [:logseq.property/default-value :logseq.property/scalar-default-value]}]
    [49 {:fix replace-special-id-ref-with-id-ref}]
-   [50 {:properties [:logseq.property.user/name :logseq.property.user/email :logseq.property.user/avatar]}]
-   [51 {:properties [:logseq.property.user/name :logseq.property.user/email :logseq.property.user/avatar]
-        :fix deprecate-logseq-user-ns}]])
+   [50 {:properties [:logseq.property.user/name :logseq.property.user/email :logseq.property.user/avatar]
+        :fix deprecate-logseq-user-ns}]
+   [51 {:classes [:logseq.class/Property :logseq.class/Tag :logseq.class/Page :logseq.class/Whiteboard]}]
+   [52 {:fix replace-block-type-with-tags}]])
 
 (let [max-schema-version (apply max (map first schema-version->updates))]
   (assert (<= db-schema/version max-schema-version))
@@ -534,17 +563,20 @@
                             (into {})
                             sqlite-create-graph/build-initial-properties*
                             (map (fn [b] (assoc b :logseq.property/built-in? true))))
-        new-classes (->> (select-keys db-class/built-in-classes classes)
-                               ;; class already exists, this should never happen
-                         (remove (fn [[k _]]
-                                   (when (d/entity db k)
-                                     (assert (str "DB migration: class already exists " k)))))
+        classes' (->> (concat [:logseq.class/Property :logseq.class/Tag :logseq.class/Page :logseq.class/Journal :logseq.class/Whiteboard] classes)
+                      distinct)
+        new-classes (->> (select-keys db-class/built-in-classes classes')
+                         ;; class already exists, this should never happen
+                         (remove (fn [[k _]] (d/entity db k)))
                          (into {})
                          (#(sqlite-create-graph/build-initial-classes* % (zipmap properties properties)))
                          (map (fn [b] (assoc b :logseq.property/built-in? true))))
+        new-class-idents (keep (fn [class]
+                                 (when-let [db-ident (:db/ident class)]
+                                   {:db/ident db-ident})) new-classes)
         fixes (when (fn? fix)
                 (fix conn search-db))
-        tx-data (if db-based? (concat new-properties new-classes fixes) fixes)
+        tx-data (if db-based? (concat new-class-idents new-properties new-classes fixes) fixes)
         tx-data' (concat
                   [(sqlite-util/kv :logseq.kv/schema-version version)]
                   tx-data)]
@@ -555,30 +587,31 @@
   "Migrate 'frontend' datascript schema and data. To add a new migration,
   add an entry to schema-version->updates and bump db-schema/version"
   [conn search-db]
-  (let [db @conn
-        version-in-db (or (:kv/value (d/entity db :logseq.kv/schema-version)) 0)]
-    (cond
-      (= version-in-db db-schema/version)
-      nil
+  (when (ldb/db-based-graph? @conn)
+    (let [db @conn
+          version-in-db (or (:kv/value (d/entity db :logseq.kv/schema-version)) 0)]
+      (cond
+        (= version-in-db db-schema/version)
+        nil
 
-      (< db-schema/version version-in-db) ; outdated client, db version could be synced from server
+        (< db-schema/version version-in-db) ; outdated client, db version could be synced from server
       ;; FIXME: notify users to upgrade to the latest version asap
-      nil
-
-      (> db-schema/version version-in-db)
-      (try
-        (let [db-based? (ldb/db-based-graph? @conn)
-              updates (keep (fn [[v updates]]
-                              (when (and (< version-in-db v) (<= v db-schema/version))
-                                [v updates]))
-                            schema-version->updates)]
-          (println "DB schema migrated from" version-in-db)
-          (doseq [[v m] updates]
-            (upgrade-version! conn search-db db-based? v m)))
-        (catch :default e
-          (prn :error (str "DB migration failed to migrate to " db-schema/version " from " version-in-db ":"))
-          (js/console.error e)
-          (throw e))))))
+        nil
+
+        (> db-schema/version version-in-db)
+        (try
+          (let [db-based? (ldb/db-based-graph? @conn)
+                updates (keep (fn [[v updates]]
+                                (when (and (< version-in-db v) (<= v db-schema/version))
+                                  [v updates]))
+                              schema-version->updates)]
+            (println "DB schema migrated from" version-in-db)
+            (doseq [[v m] updates]
+              (upgrade-version! conn search-db db-based? v m)))
+          (catch :default e
+            (prn :error (str "DB migration failed to migrate to " db-schema/version " from " version-in-db ":"))
+            (js/console.error e)
+            (throw e)))))))
 
 ;; Backend migrations
 ;; ==================

+ 1 - 1
src/main/frontend/worker/db_worker.cljs

@@ -318,7 +318,7 @@
 
         (when-not db-based?
           (try
-            (when-not (ldb/page-exists? @conn common-config/views-page-name "page")
+            (when-not (ldb/page-exists? @conn common-config/views-page-name #{:logseq.class/Page})
               (ldb/transact! conn (sqlite-create-graph/build-initial-views)))
             (catch :default _e)))
 

+ 7 - 14
src/main/frontend/worker/export.cljs

@@ -6,8 +6,7 @@
             [logseq.graph-parser.property :as gp-property]
             [logseq.outliner.tree :as otree]
             [cljs-bean.core :as bean]
-            [logseq.db.sqlite.util :as sqlite-util]
-            [clojure.string :as string]))
+            [logseq.db.sqlite.util :as sqlite-util]))
 
 (defn- safe-keywordize
   [block]
@@ -64,23 +63,17 @@
                          (:keys result)))))
            (group-by first)
            (mapcat (fn [[_id col]]
-                     (let [type (some (fn [[_e a v _t]]
-                                        (when (= a :block/type)
-                                          v)) col)
-                           ident (some (fn [[_e a v _t]]
+                     (let [ident (some (fn [[_e a v _t]]
                                          (when (= a :db/ident)
-                                           v)) col)]
+                                           v)) col)
+                           journal (some (fn [[_e a v _t]]
+                                           (when (= a :block/journal-day)
+                                             v)) col)]
                        (map
                         (fn [[e a v t]]
                           (cond
                             (and (contains? #{:block/title :block/name} a)
-                                 (or
-                                  ;; normal page or block
-                                  (not (contains? #{"class" "property" "journal" "closed value"} type))
-                                  ;; class/property created by user
-                                  (and ident
-                                       (contains? #{"class" "property"} type)
-                                       (not (string/starts-with? (namespace ident) "logseq")))))
+                                 (not (or ident journal)))
                             [e a (str "debug " e) t]
 
                             (= a :block/uuid)

+ 2 - 2
src/main/frontend/worker/handler/page.cljs

@@ -30,8 +30,8 @@
 
    * :create-first-block?      - when true, create an empty block if the page is empty.
    * :uuid                     - when set, use this uuid instead of generating a new one.
-   * :class?                   - when true, adds a :block/type 'class'
-   * :whiteboard?              - when true, adds a :block/type 'whiteboard'
+   * :class?                   - when true, adds a :block/tags ':logseq.class/Tag'
+   * :whiteboard?              - when true, adds a :block/tags ':logseq.class/Whiteboard'
    * :tags                     - tag uuids that are added to :block/tags
    * :persist-op?              - when true, add an update-page op
    * :properties               - properties to add to the page

+ 51 - 36
src/main/frontend/worker/handler/page/db_based/page.cljs

@@ -13,30 +13,30 @@
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.text :as text]
-            [logseq.outliner.validate :as outliner-validate]))
+            [logseq.outliner.validate :as outliner-validate]
+            [logseq.db.frontend.entity-util :as entity-util]
+            [logseq.db.frontend.malli-schema :as db-malli-schema]))
 
 (defn- build-page-tx [conn properties page {:keys [whiteboard? class? tags]}]
   (when (:block/uuid page)
-    (let [page (assoc page :block/type (cond class? "class"
-                                             whiteboard? "whiteboard"
-                                             (:block/type page) (:block/type page)
-                                             :else "page"))
-          page' (cond-> page
-                  (seq tags)
-                  (update :block/tags
-                          (fnil into [])
-                          (mapv (fn [tag]
-                                  (let [v (if (uuid? tag)
-                                            (d/entity @conn [:block/uuid tag])
-                                            tag)]
-                                    (cond
-                                      (de/entity? v)
-                                      (:db/id v)
-                                      (map? v)
-                                      (:db/id v)
-                                      :else
-                                      v)))
-                                tags)))
+    (let [type-tag (cond class? :logseq.class/Tag
+                         whiteboard? :logseq.class/Whiteboard
+                         :else :logseq.class/Page)
+          tags' (if (:block/journal-day page) tags (conj tags type-tag))
+          page' (update page :block/tags
+                        (fnil into [])
+                        (mapv (fn [tag]
+                                (let [v (if (uuid? tag)
+                                          (d/entity @conn [:block/uuid tag])
+                                          tag)]
+                                  (cond
+                                    (de/entity? v)
+                                    (:db/id v)
+                                    (map? v)
+                                    (:db/id v)
+                                    :else
+                                    v)))
+                              tags'))
           property-vals-tx-m
           ;; Builds property values for built-in properties like logseq.property.pdf/file
           (db-property-build/build-property-values-tx-m
@@ -95,10 +95,12 @@
 
 (defn- split-namespace-pages
   [db page date-formatter]
-  (let [{:block/keys [title] block-uuid :block/uuid block-type :block/type} page]
+  (let [{:block/keys [title] block-uuid :block/uuid} page]
     (->>
-     (if (and (contains? #{"page" "class"} block-type) (ns-util/namespace-page? title))
-       (let [class? (= block-type "class")
+     (if (and (or (entity-util/class? page)
+                  (entity-util/page? page))
+              (ns-util/namespace-page? title))
+       (let [class? (entity-util/class? page)
              parts (->> (string/split title ns-util/parent-re)
                         (map string/trim)
                         (remove string/blank?))
@@ -168,15 +170,24 @@
   (let [db @conn
         date-formatter (:logseq.property.journal/title-format (d/entity db :logseq.class/Journal))
         title (sanitize-title title*)
-        type (cond class?
-                   "class"
-                   whiteboard?
-                   "whiteboard"
-                   today-journal?
-                   "journal"
-                   :else
-                   "page")]
-    (when-not (ldb/page-exists? db title type)
+        types (cond class?
+                    #{:logseq.class/Tag}
+                    whiteboard?
+                    #{:logseq.class/Whiteboard}
+                    today-journal?
+                    #{:logseq.class/Journal}
+                    :else
+                    #{:logseq.class/Page})]
+    (if-let [existing-page-id (first (ldb/page-exists? db title types))]
+      (let [existing-page (d/entity db existing-page-id)
+            tx-meta {:persist-op? persist-op?
+                     :outliner-op :save-block}]
+        (when (and class?
+                   (not (ldb/class? existing-page))
+                   (or (ldb/property? existing-page) (ldb/internal-page? existing-page)))
+          ;; Convert existing user property or page to class
+          (let [tx-data (db-class/build-new-class db (select-keys existing-page [:block/title :block/uuid :db/ident :block/created-at]))]
+            (ldb/transact! conn tx-data tx-meta))))
       (let [format    :markdown
             page      (-> (gp-block/page-name->map title @conn true date-formatter
                                                    {:class? class?
@@ -189,9 +200,12 @@
                              (let [pages (split-namespace-pages db page date-formatter)]
                                [(last pages) (butlast pages)])
                              [page nil])]
-        (when page
+        (when (and page (or (nil? (:db/ident page))
+                            ;; New page creation must not override built-in entities
+                            (not (db-malli-schema/internal-ident? (:db/ident page)))))
           ;; Don't validate journal names because they can have '/'
-          (when (not= "journal" type)
+          (when-not (or (contains? types :logseq.class/Journal)
+                        (contains? (set (:block/tags page)) :logseq.class/Journal))
             (outliner-validate/validate-page-title-characters (str (:block/title page)) {:node page})
             (doseq [parent parents]
               (outliner-validate/validate-page-title-characters (str (:block/title parent)) {:node parent})))
@@ -205,7 +219,8 @@
                                       page-txs)
                                  (build-first-block-tx (:block/uuid (first page-txs)) format))
                 txs      (concat
-                          parents
+                          ;; transact doesn't support entities
+                          (remove de/entity? parents)
                           page-txs
                           first-block-tx)]
             (when (seq txs)

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

@@ -22,7 +22,7 @@
 
 (def ^:private watched-attrs
   #{:block/title :block/created-at :block/updated-at :block/alias
-    :block/tags :block/type :block/schema :block/link :block/journal-day
+    :block/tags :block/schema :block/link :block/journal-day
     :property/schema.classes :property.value/content
     :db/index :db/valueType :db/cardinality})
 

+ 0 - 1
src/main/frontend/worker/rtc/remote_update.cljs

@@ -366,7 +366,6 @@
     :block/updated-at
     :block/created-at
     :block/alias
-    :block/type
     :block/schema
     :block/tags
     :block/link

+ 0 - 7
src/main/frontend/worker/search.cljs

@@ -332,13 +332,6 @@ DROP TRIGGER IF EXISTS blocks_au;
   (drop-tables-and-triggers! db)
   (create-tables-and-triggers! db))
 
-(comment
-  (defn- property-value-when-closed
-    "Returns property value if the given entity is type 'closed value' or nil"
-    [ent]
-    (when (= (:block/type ent) "closed value")
-      (:block/title ent))))
-
 (comment
   (defn- get-db-properties-str
     "Similar to db-pu/readable-properties but with a focus on making property values searchable"

+ 17 - 16
src/test/frontend/db/db_based_model_test.cljs

@@ -4,8 +4,10 @@
             [frontend.db :as db]
             [frontend.test.helper :as test-helper]
             [datascript.core :as d]
-            [logseq.outliner.property :as outliner-property]
-            [logseq.db.frontend.class :as db-class]))
+            [logseq.db.frontend.class :as db-class]
+            [logseq.db :as ldb]
+            [logseq.db.test.helper :as db-test]
+            [frontend.db.conn :as conn]))
 
 (def repo test-helper/test-db-name-db-version)
 
@@ -27,7 +29,9 @@
         _ (test-helper/create-page! "class2" opts)]
     (is (= (set
             (concat
-             (map :title (vals db-class/built-in-classes))
+             (map :title (vals (remove (fn [[ident _]]
+                                         (contains? ldb/private-tags ident))
+                                       db-class/built-in-classes)))
              ["class1" "class2"]))
            (set (map :block/title (model/get-all-classes repo)))))))
 
@@ -51,19 +55,16 @@
               (:db/id (db/entity [:block/uuid sbid]))])))))
 
 (deftest get-classes-with-property-test
-  (let [opts {:redirect? false :create-first-block? false :class? true}
-        _ (test-helper/create-page! "class1" opts)
-        _ (test-helper/create-page! "class2" opts)
-        class1 (db/get-case-page "class1")
-        class2 (db/get-case-page "class2")
-        conn (db/get-db false)]
-    (outliner-property/upsert-property! conn :user.property/property-1 {:type :node} {})
-    (outliner-property/class-add-property! conn (:db/id class1) :user.property/property-1)
-    (outliner-property/class-add-property! conn (:db/id class2) :user.property/property-1)
-    (let [property (db/entity :user.property/property-1)
-          classes (model/get-classes-with-property (:db/ident property))]
-      (is (= (set (map :db/id classes))
-             #{(:db/id class1) (:db/id class2)})))))
+  (let [conn (db-test/create-conn-with-blocks
+              {:properties {:prop1 {:block/schema {:type :default}}}
+               :classes
+               {:Class1 {:build/schema-properties [:prop1]}
+                :Class2 {:build/schema-properties [:prop1]}}})
+        property (d/entity @conn :user.property/prop1)
+        classes (with-redefs [conn/get-db (constantly @conn)]
+                  (model/get-classes-with-property (:db/ident property)))]
+    (is (= ["Class1" "Class2"]
+           (map :block/title classes)))))
 
 (deftest hidden-page-test
   (let [opts {:redirect? false :create-first-block? false}

+ 23 - 11
src/test/frontend/worker/handler/page/db_based/page_test.cljs

@@ -9,12 +9,8 @@
   (let [conn (db-test/create-conn)
         _ (worker-db-page/create! conn "movie" {:class? true})
         _ (worker-db-page/create! conn "Movie" {:class? true})
-        movie-class (->> (d/q '[:find [(pull ?b [*]) ...] :in $ ?title :where [?b :block/title ?title]]
-                              @conn "movie")
-                         first)
-        Movie-class (->> (d/q '[:find [(pull ?b [*]) ...] :in $ ?title :where [?b :block/title ?title]]
-                              @conn "Movie")
-                         first)]
+        movie-class (ldb/get-case-page @conn "movie")
+        Movie-class (ldb/get-case-page @conn "Movie")]
 
     (is (ldb/class? movie-class) "Creates a class")
     (is (ldb/class? Movie-class) "Creates another class with a different case sensitive name")
@@ -44,9 +40,12 @@
             "Child class with new parent has correct parents")
 
         (worker-db-page/create! conn "foo/class1/baz3" {:split-namespace? true})
-        (is (= #{"class" "page"}
-               (set (d/q '[:find [?type ...]
-                           :where [?b :block/type ?type] [?b :block/title "class1"]] @conn)))
+        (is (= #{"Tag" "Page"}
+               (set (d/q '[:find [?tag-title ...]
+                           :where
+                           [?b :block/title "class1"]
+                           [?b :block/tags ?t]
+                           [?t :block/title ?tag-title]] @conn)))
             "Using an existing class page in a multi-parent namespace doesn't allow a page to have a class parent and instead creates a new page")))
 
     (testing "Child pages with same name and different parents"
@@ -79,10 +78,23 @@
   (let [conn (db-test/create-conn)
         [_ page-uuid] (worker-db-page/create! conn "fooz" {})]
     (is (= "fooz" (:block/title (d/entity @conn [:block/uuid page-uuid])))
-        "Valid page created")
+        "Page created correctly")
 
     (is (thrown-with-msg?
          js/Error
          #"can't include \"/"
          (worker-db-page/create! conn "foo/bar" {}))
-        "Page can't have '/'n title")))
+        "Page can't have '/'n title")))
+
+(deftest create-journal
+  (let [conn (db-test/create-conn)
+        [_ page-uuid] (worker-db-page/create! conn "Dec 16th, 2024" {})]
+
+    (is (= "Dec 16th, 2024" (:block/title (d/entity @conn [:block/uuid page-uuid])))
+        "Journal created correctly")
+
+    (is (= [:logseq.class/Journal]
+           (->> (d/entity @conn [:block/uuid page-uuid])
+                :block/tags
+                (map #(:db/ident (d/entity @conn (:db/id %))))))
+        "New journal only has Journal tag")))

+ 32 - 33
src/test/frontend/worker/rtc/db_listener_test.cljs

@@ -9,16 +9,14 @@
             [frontend.worker.rtc.db-listener :as subject]
             [frontend.worker.rtc.fixture :as r.fixture]
             [frontend.worker.state :as worker-state]
-            [logseq.db.frontend.schema :as db-schema]
             [logseq.outliner.batch-tx :as batch-tx]
-            [logseq.outliner.core :as outliner-core]))
+            [logseq.outliner.core :as outliner-core]
+            [logseq.db.test.helper :as db-test]))
 
 (t/use-fixtures :each
   test-helper/db-based-start-and-destroy-db-map-fixture
   r.fixture/listen-test-db-to-gen-rtc-ops-fixture)
 
-(def empty-db (d/empty-db db-schema/schema-for-db-based-graph))
-
 (defn- tx-data=>e->a->add?->v->t
   [tx-data]
   (let [datom-vec-coll (map vec tx-data)
@@ -27,11 +25,11 @@
 
 (deftest entity-datoms=>ops-test
   (testing "remove whiteboard page-block"
-    (let [conn (d/conn-from-db empty-db)
+    (let [conn (db-test/create-conn)
           block-uuid (random-uuid)
           _create-whiteboard-page-block
           (d/transact! conn [{:block/uuid block-uuid
-                              :block/type "whiteboard"
+                              :block/tags :logseq.class/Whiteboard
                               :block/name "block-name"
                               :block/title "BLOCK-NAME"}])
           remove-whiteboard-page-block
@@ -44,20 +42,20 @@
              (map (fn [[op-type _t op-value]] [op-type op-value]) r)))))
 
   (testing "update-schema op"
-    (let [conn (d/conn-from-db empty-db)
-          tx-data [[:db/add 69 :db/index true]
-                   [:db/add 69 :block/uuid #uuid "66558abf-6512-469d-9e83-8f1ba0be9305"]
-                   [:db/add 69 :db/valueType :db.type/ref]
-                   [:db/add 69 :block/updated-at 1716882111476]
-                   [:db/add 69 :block/created-at 1716882111476]
-                   [:db/add 69 :block/schema {:type :number}]
-                   [:db/add 69 :block/format :markdown]
-                   [:db/add 69 :db/cardinality :db.cardinality/one]
-                   [:db/add 69 :db/ident :user.property/qqq]
-                   [:db/add 69 :block/type "property"]
-                   [:db/add 69 :block/order "b0T"]
-                   [:db/add 69 :block/name "qqq"]
-                   [:db/add 69 :block/title "qqq"]]
+    (let [conn (db-test/create-conn)
+          tx-data [[:db/add 1000000 :db/index true]
+                   [:db/add 1000000 :block/uuid #uuid "66558abf-6512-469d-9e83-8f1ba0be9305"]
+                   [:db/add 1000000 :db/valueType :db.type/ref]
+                   [:db/add 1000000 :block/updated-at 1716882111476]
+                   [:db/add 1000000 :block/created-at 1716882111476]
+                   [:db/add 1000000 :block/schema {:type :number}]
+                   [:db/add 1000000 :block/format :markdown]
+                   [:db/add 1000000 :db/cardinality :db.cardinality/one]
+                   [:db/add 1000000 :db/ident :user.property/qqq]
+                   [:db/add 1000000 :block/tags :logseq.class/Property]
+                   [:db/add 1000000 :block/order "b0T"]
+                   [:db/add 1000000 :block/name "qqq"]
+                   [:db/add 1000000 :block/title "qqq"]]
           {:keys [db-before db-after tx-data]} (d/transact! conn tx-data)
           ops (#'subject/entity-datoms=>ops db-before db-after
                                             (tx-data=>e->a->add?->v->t tx-data)
@@ -72,10 +70,11 @@
                        [:block/updated-at "[\"~#'\",1716882111476]"]
                        [:block/created-at "[\"~#'\",1716882111476]"]
                        [:block/schema "[\"^ \",\"~:type\",\"~:number\"]"]
+                       [:block/tags #uuid "00000002-1038-7670-4800-000000000000"]
                        [:block/title "[\"~#'\",\"qqq\"]"]
                        [:db/cardinality "[\"~#'\",\"~:db.cardinality/one\"]"]
                        ;; [:db/ident "[\"~#'\",\"~:user.property/qqq\"]"]
-                       [:block/type "[\"~#'\",\"property\"]"]]}]]
+                       ]}]]
            (map (fn [[op-type _t op-value]]
                   [op-type (cond-> op-value
                              (:av-coll op-value)
@@ -83,16 +82,16 @@
                 ops)))))
 
   (testing "create user-class"
-    (let [conn (d/conn-from-db empty-db)
-          tx-data [[:db/add 62 :block/uuid #uuid "66856a29-6eb3-4122-af97-8580a853c6a6" 536870954]
-                   [:db/add 62 :block/updated-at 1720019497643 536870954]
-                   [:db/add 62 :logseq.property/parent 4 536870954]
-                   [:db/add 62 :block/created-at 1720019497643 536870954]
-                   [:db/add 62 :block/format :markdown 536870954]
-                   [:db/add 62 :db/ident :user.class/zzz 536870954]
-                   [:db/add 62 :block/type "class" 536870954]
-                   [:db/add 62 :block/name "zzz" 536870954]
-                   [:db/add 62 :block/title "zzz" 536870954]]
+    (let [conn (db-test/create-conn)
+          tx-data [[:db/add 1000000 :block/uuid #uuid "66856a29-6eb3-4122-af97-8580a853c6a6" 536870954]
+                   [:db/add 1000000 :block/updated-at 1720019497643 536870954]
+                   [:db/add 1000000 :logseq.property/parent :logseq.class/Root 536870954]
+                   [:db/add 1000000 :block/created-at 1720019497643 536870954]
+                   [:db/add 1000000 :block/format :markdown 536870954]
+                   [:db/add 1000000 :db/ident :user.class/zzz 536870954]
+                   [:db/add 1000000 :block/tags :logseq.class/Tag 536870954]
+                   [:db/add 1000000 :block/name "zzz" 536870954]
+                   [:db/add 1000000 :block/title "zzz" 536870954]]
           {:keys [db-before db-after tx-data]} (d/transact! conn tx-data)
           ops (#'subject/entity-datoms=>ops db-before db-after
                                             (tx-data=>e->a->add?->v->t tx-data)
@@ -103,9 +102,9 @@
                       :av-coll
                       [[:block/updated-at "[\"~#'\",1720019497643]"]
                        [:block/created-at "[\"~#'\",1720019497643]"]
+                       [:block/tags #uuid "00000002-5389-0208-3000-000000000000"]
                        [:block/title "[\"~#'\",\"zzz\"]"]
-                       [:block/type "[\"~#'\",\"class\"]"]
-                       [:logseq.property/parent "[\"~#'\",4]"]
+                       [:logseq.property/parent #uuid "00000002-2737-8382-7000-000000000000"]
                        ;;1. shouldn't have :db/ident, :db/ident is special, will be handled later
                        ]}]]
            (map (fn [[op-type _t op-value]]

+ 1 - 1
src/test/frontend/worker/rtc/remote_update_test.cljs

@@ -9,7 +9,7 @@
 (deftest remote-op-value->tx-data-test
   (let [[block-uuid ref-uuid1 ref-uuid2] (repeatedly random-uuid)
         db (d/db-with (d/empty-db db-schema/schema-for-db-based-graph)
-                      (sqlite-create-graph/build-db-initial-data ""))]
+                      (sqlite-create-graph/build-db-initial-data "{}" {}))]
     (testing ":block/title"
       (let [db (d/db-with db [{:block/uuid block-uuid
                                :block/title "local-content"}])

+ 5 - 5
static/yarn.lock

@@ -1831,7 +1831,7 @@ [email protected]:
     electron-log "^4.2.3"
     node-addon-api "^2.0.0"
 
[email protected]:
+electron-devtools-installer@^3.2.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-3.2.0.tgz#acc48d24eb7033fe5af284a19667e73b78d406d0"
   integrity sha512-t3UczsYugm4OAbqvdImMCImIMVdFzJAHgbwHpkl5jmfu1izVgUcP/mnrPqJIpEeCK1uZGpt+yHgWEN+9EwoYhQ==
@@ -4626,13 +4626,13 @@ stream-buffers@~2.2.0:
     strip-ansi "^6.0.1"
 
 string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3, string-width@^5.0.0, string-width@^5.1.2:
-  version "4.2.3"
-  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
-  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+  version "4.2.0"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.0.tgz#952182c46cc7b2c313d1596e623992bd163b72b5"
+  integrity sha512-zUz5JD+tgqtuDjMhwIg5uFVV3dtqZ9yQJlZVfq4I01/K5Paj5UHj7VyrQOJvzawSVlKpObApbfD0Ed6yJc+1eg==
   dependencies:
     emoji-regex "^8.0.0"
     is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.1"
+    strip-ansi "^6.0.0"
 
 string_decoder@^1.1.1:
   version "1.3.0"