Просмотр исходного кода

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

charlie 1 год назад
Родитель
Сommit
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/metosin/malli
 .clj-kondo/rewrite-clj
 .clj-kondo/rewrite-clj
 .clj-kondo/taoensso
 .clj-kondo/taoensso
+.clj-kondo/funcool
 /libs/dist/
 /libs/dist/
 charlie/
 charlie/
 .vscode
 .vscode

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

@@ -15,6 +15,8 @@ logseq.db.frontend.inputs/resolve-input
 ;; API
 ;; API
 logseq.db.frontend.class/build-new-class
 logseq.db.frontend.class/build-new-class
 ;; API
 ;; API
+logseq.db.frontend.class/page-children-classes
+;; API
 logseq.db.frontend.db-ident/ensure-unique-db-ident
 logseq.db.frontend.db-ident/ensure-unique-db-ident
 ;; API
 ;; API
 logseq.db.sqlite.build/create-blocks
 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.sqlite.util :as sqlite-util]
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property :as db-property]
             [logseq.common.util.namespace :as ns-util]
             [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?]))
   (:refer-clojure :exclude [object?]))
 
 
 (defonce *transact-fn (atom nil))
 (defonce *transact-fn (atom nil))
@@ -40,11 +41,25 @@
              m))
              m))
          tx-data)))
          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!
 (defn transact!
   "`repo-or-conn`: repo for UI thread and conn for worker/node"
   "`repo-or-conn`: repo for UI thread and conn for worker/node"
   ([repo-or-conn tx-data]
   ([repo-or-conn tx-data]
    (transact! repo-or-conn tx-data nil))
    (transact! repo-or-conn tx-data nil))
   ([repo-or-conn tx-data tx-meta]
   ([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]
    (let [tx-data (map (fn [m]
                         (if (map? m)
                         (if (map? m)
                           (dissoc m :block/children :block/meta :block/top? :block/bottom? :block/anchor
                           (dissoc m :block/children :block/meta :block/top? :block/bottom? :block/anchor
@@ -85,6 +100,10 @@
 (def object? entity-util/object?)
 (def object? entity-util/object?)
 (def asset? entity-util/asset?)
 (def asset? entity-util/asset?)
 (def public-built-in-property? db-property/public-built-in-property?)
 (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
 (defn sort-by-order
   [blocks]
   [blocks]
@@ -176,33 +195,39 @@
 (def db-based-graph? entity-util/db-based-graph?)
 (def db-based-graph? entity-util/db-based-graph?)
 
 
 (defn page-exists?
 (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
   (when page-name
     (if (db-based-graph? db)
     (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)]))))
       (d/entity db [:block/name (common-util/page-name-sanity-lc page-name)]))))
 
 
 (defn get-page
 (defn get-page
@@ -448,7 +473,7 @@
    (d/datoms db :avet :block/name)
    (d/datoms db :avet :block/name)
    (keep (fn [d]
    (keep (fn [d]
            (let [e (d/entity db (:e d))]
            (let [e (d/entity db (:e d))]
-             (when-not (hidden? e)
+             (when-not (or (hidden? e) (internal-tags (:db/ident e)))
                e))))))
                e))))))
 
 
 (defn built-in?
 (defn built-in?
@@ -535,7 +560,7 @@
 
 
 (defn get-all-properties
 (defn get-all-properties
   [db]
   [db]
-  (->> (d/datoms db :avet :block/type "property")
+  (->> (d/datoms db :avet :block/tags :logseq.class/Property)
        (map (fn [d]
        (map (fn [d]
               (d/entity db (:e d))))))
               (d/entity db (:e d))))))
 
 
@@ -554,7 +579,7 @@
 
 
 (defn get-title-with-parents
 (defn get-title-with-parents
   [entity]
   [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)
     (let [parents' (->> (get-page-parents entity)
                         (remove (fn [e] (= :logseq.class/Root (:db/ident e))))
                         (remove (fn [e] (= :logseq.class/Root (:db/ident e))))
                         vec)]
                         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"
   "Class related fns for DB graphs and frontend/datascript usage"
   (:require [logseq.db.sqlite.util :as sqlite-util]
   (:require [logseq.db.sqlite.util :as sqlite-util]
             [logseq.db.frontend.db-ident :as db-ident]
             [logseq.db.frontend.db-ident :as db-ident]
+            [clojure.set :as set]
             [flatland.ordered.map :refer [ordered-map]]))
             [flatland.ordered.map :refer [ordered-map]]))
 
 
+;; Main class vars
+;; ===============
+
 (def ^:large-vars/data-var built-in-classes
 (def ^:large-vars/data-var built-in-classes
   "Map of built-in classes for db graphs with their :db/ident as keys"
   "Map of built-in classes for db graphs with their :db/ident as keys"
   (ordered-map
   (ordered-map
    :logseq.class/Root {:title "Root Tag"}
    :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
    :logseq.class/Journal
    {:title "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
    :logseq.class/Query
    {:title "Query"
    {:title "Query"
@@ -66,6 +81,31 @@
 ;; TODO: Add more classes such as :book, :paper, :movie, :music, :project)
 ;; 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
 (defn create-user-class-ident-from-name
   "Creates a class :db/ident for a default user namespace.
   "Creates a class :db/ident for a default user namespace.
    NOTE: Only use this when creating a db-ident for a new class."
    NOTE: Only use this when creating a db-ident for a new class."
@@ -78,5 +118,6 @@
   [db page-m]
   [db page-m]
   {:pre [(string? (:block/title page-m))]}
   {:pre [(string? (:block/title page-m))]}
   (let [db-ident (create-user-class-ident-from-name (: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'))))
     (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]
   [^Entity e k default-value]
   (let [db (.-db e)
   (let [db (.-db e)
         db-based? (db-based-graph? db)]
         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)
       (get-journal-title db e)
       (let [search? (get (.-kv e) :block.temp/search?)]
       (let [search? (get (.-kv e) :block.temp/search?)]
         (or
         (or
@@ -65,7 +65,7 @@
        (let [db (.-db e)]
        (let [db (.-db e)]
          (case k
          (case k
            :block/raw-title
            :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)
              (get-journal-title db e)
              (lookup-entity e :block/title default-value))
              (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
 (ns logseq.db.frontend.entity-util
   "Lower level entity util fns used across db namespaces"
   "Lower level entity util fns used across db namespaces"
   (:require [datascript.core :as d]
   (:require [datascript.core :as d]
-            [clojure.string :as string])
+            [clojure.string :as string]
+            [datascript.impl.entity :as de])
   (:refer-clojure :exclude [object?]))
   (:refer-clojure :exclude [object?]))
 
 
 (defn db-based-graph?
 (defn db-based-graph?
@@ -10,36 +11,60 @@
   (when db
   (when db
     (= "db" (:kv/value (d/entity db :logseq.kv/db-type)))))
     (= "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?
 (defn internal-page?
   [entity]
   [entity]
-  (= (:block/type entity) "page"))
+  (has-tag? entity :logseq.class/Page))
 
 
 (defn class?
 (defn class?
   [entity]
   [entity]
-  (= (:block/type entity) "class"))
+  (or (= (:db/ident entity) :logseq.class/Tag)
+      (has-tag? entity :logseq.class/Tag)))
 
 
 (defn property?
 (defn property?
   [entity]
   [entity]
-  (= (:block/type entity) "property"))
-
-(defn closed-value?
-  [entity]
-  (= (:block/type entity) "closed value"))
+  (has-tag? entity :logseq.class/Property))
 
 
 (defn whiteboard?
 (defn whiteboard?
   "Given a page entity or map, check if it is a whiteboard page"
   "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?
 (defn journal?
   "Given a page entity or map, check if it is a journal page"
   "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?
 (defn asset?
   "Given an entity or map, check if it is an asset block"
   "Given an entity or map, check if it is an asset block"
@@ -52,9 +77,19 @@
   (when page
   (when page
     (if (string? page)
     (if (string? page)
       (string/starts-with? page "$$$")
       (string/starts-with? page "$$$")
-      (when (map? page)
+      (when (or (map? page) (de/entity? page))
         (false? (get-in page [:block/schema :public?]))))))
         (false? (get-in page [:block/schema :public?]))))))
 
 
 (defn object?
 (defn object?
   [node]
   [node]
   (seq (:block/tags 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 (:db/ident (d/entity db property-val))))
     (= :logseq.property/empty-placeholder 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
 (defn validate-property-value
   "Validates the property value in a property tuple. The property value is
   "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
   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"
   validate-fn varies by property type"
   [db validate-fn [{:block/keys [schema] :as property} property-val] & {:keys [new-closed-value?]}]
   [db validate-fn [{:block/keys [schema] :as property} property-val] & {:keys [new-closed-value?]}]
   ;; For debugging
   ;; 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))
   (let [validate-fn' (if (db-property-type/property-types-with-db (:type schema))
                        (fn [value]
                        (fn [value]
                          (validate-fn db value {:new-closed-value? new-closed-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]))
    (set (get-in db-class/built-in-classes [:logseq.class/Asset :schema :required-properties]))
    #{:logseq.property/created-from-property}))
    #{: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
 (defn update-properties-in-ents
   "Prepares properties in entities to be validated by DB schema"
   "Prepares properties in entities to be validated by DB schema"
   [db ents]
   [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
 (defn datoms->entity-maps
   "Returns entity maps for given :eavt datoms indexed by db/id. Optional keys:
   "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}))
   (mapv (fn [[db-id m]] (with-meta m {:db/id db-id}))
         (datoms->entity-maps datoms)))
         (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)
 (assert (every? #(re-find #"^(block|logseq\.)" (namespace %)) db-property/db-attribute-properties)
         "All db-attribute idents start with an internal namespace")
         "All db-attribute idents start with an internal namespace")
 (assert (every? #(re-find #"^logseq\." %) logseq-ident-namespaces)
 (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
 ;; These schemas should be data vars to remain as simple and reusable as possible
 
 
-
 (def ^:dynamic *db-for-validate-fns*
 (def ^:dynamic *db-for-validate-fns*
   "Used by validate-fns which need db as input"
   "Used by validate-fns which need db as input"
   nil)
   nil)
 
 
 (def property-tuple
 (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
   (into
    [:multi {:dispatch #(-> % first :block/schema :type)}]
    [:multi {:dispatch #(-> % first :block/schema :type)}]
    (map (fn [[prop-type value-schema]]
    (map (fn [[prop-type value-schema]]
@@ -211,6 +226,24 @@
   property with its property value that is valid for its type"
   property with its property value that is valid for its type"
   [:sequential property-tuple])
   [: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
 (def page-or-block-attrs
   "Common attributes for page and normal blocks"
   "Common attributes for page and normal blocks"
   [[:block/uuid :uuid]
   [[:block/uuid :uuid]
@@ -219,8 +252,8 @@
    [:block/format [:enum :markdown]]
    [:block/format [:enum :markdown]]
    ;; Injected by update-properties-in-ents
    ;; Injected by update-properties-in-ents
    [:block/properties {:optional true} block-properties]
    [:block/properties {:optional true} block-properties]
+   [:block/tags {:optional true} block-tags]
    [:block/refs {:optional true} [:set :int]]
    [:block/refs {:optional true} [:set :int]]
-   [:block/tags {:optional true} [:set :int]]
    [:block/tx-id {:optional true} :int]
    [:block/tx-id {:optional true} :int]
    [:block/collapsed? {:optional true} :boolean]])
    [:block/collapsed? {:optional true} :boolean]])
 
 
@@ -228,12 +261,7 @@
   "Common attributes for pages"
   "Common attributes for pages"
   [[:block/name :string]
   [[:block/name :string]
    [:block/title :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
 (def property-attrs
   "Common attributes for properties"
   "Common attributes for properties"
@@ -302,7 +330,8 @@
   (vec
   (vec
    (concat
    (concat
     [:map
     [: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]]
      [:block/schema user-property-schema]]
     property-attrs
     property-attrs
     page-attrs
     page-attrs
@@ -364,8 +393,7 @@
   (vec
   (vec
    (concat
    (concat
     [:map]
     [:map]
-    [[:block/type [:= "closed value"]]
-     ;; for built-in properties
+    [;; for built-in properties
      [:db/ident {:optional true} logseq-property-ident]
      [:db/ident {:optional true} logseq-property-ident]
      [:block/title {:optional true} :string]
      [:block/title {:optional true} :string]
      [:property.value/content {:optional true} [:or :string :double]]
      [:property.value/content {:optional true} [:or :string :double]]
@@ -436,27 +464,30 @@
   (into
   (into
    [:multi {:dispatch (fn [d]
    [:multi {:dispatch (fn [d]
                         ;; order matters as some block types are a subset of others e.g. :whiteboard
                         ;; 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
    {:property property-page
     :class class-page
     :class class-page
     :hidden hidden-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*))
 (let [malli-many-ref-attrs (->> (concat property-attrs page-attrs block-attrs page-or-block-attrs (rest closed-value-block*))
                                 (filter #(= (last %) [:set :int]))
                                 (filter #(= (last %) [:set :int]))
                                 (map first)
                                 (map first)
+                                (into db-property/public-db-attribute-properties)
                                 set)]
                                 set)]
   (when-let [undeclared-ref-attrs (seq (remove malli-many-ref-attrs db-schema/card-many-ref-type-attributes))]
   (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: "
     (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))
    (reset-max-key! *max-key key))
   ([max-key-atom key]
   ([max-key-atom key]
    (when (and key (or (nil? @max-key-atom)
    (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
 (defn gen-key
   ([]
   ([]
@@ -50,7 +50,7 @@
                 (when (and (< (compare (:block/order e) (:block/order value)) 0)
                 (when (and (< (compare (:block/order e) (:block/order value)) 0)
                            (not= (:db/id e) (:db/id value)))
                            (not= (:db/id e) (:db/id value)))
                   (:block/order e))) values))
                   (: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))))
                             (map (fn [d] (d/entity db (:e d))))
                             (sort-by :block/order)
                             (sort-by :block/order)
                             reverse)]
                             reverse)]
@@ -68,7 +68,7 @@
                 (when (and (> (compare (:block/order e) (:block/order value)) 0)
                 (when (and (> (compare (:block/order e) (:block/order value)) 0)
                            (not= (:db/id e) (:db/id value)))
                            (not= (:db/id e) (:db/id value)))
                   (:block/order e))) values))
                   (: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))))
                             (map (fn [d] (d/entity db (:e d))))
                             (sort-by :block/order))]
                             (sort-by :block/order))]
         (some (fn [property]
         (some (fn [property]

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

@@ -48,12 +48,6 @@
                                         :schema {:type :any
                                         :schema {:type :any
                                                  :public? false
                                                  :public? false
                                                  :hide? true}}
                                                  :hide? true}}
-   :block/type           {:title "Node Type"
-                          :attribute :block/type
-                          :schema {:type :string
-                                   :public? false
-                                   :hide? true}
-                          :queryable? true}
    :block/schema         {:title "Node schema"
    :block/schema         {:title "Node schema"
                           :attribute :block/schema
                           :attribute :block/schema
                           :schema {:type :map
                           :schema {:type :map
@@ -451,13 +445,18 @@
 
 
 (def db-attribute-properties
 (def db-attribute-properties
   "Internal properties that are also db schema attributes"
   "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/order :block/collapsed? :block/page
     :block/refs :block/path-refs :block/link
     :block/refs :block/path-refs :block/link
     :block/title :block/closed-value-property
     :block/title :block/closed-value-property
     :block/created-at :block/updated-at
     :block/created-at :block/updated-at
     :logseq.property.attribute/kv-value :logseq.property.attribute/property-schema-classes :logseq.property.attribute/property-value-content})
     :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
 (def private-db-attribute-properties
   "db-attribute properties that are not visible to user"
   "db-attribute properties that are not visible to user"
   (->> db-attribute-properties
   (->> db-attribute-properties
@@ -472,11 +471,6 @@
   "Property values that shouldn't be updated"
   "Property values that shouldn't be updated"
   #{:logseq.property/built-in?})
   #{: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
 (def logseq-property-namespaces
   #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.property.fsrs" "logseq.task"
   #{"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"
     "logseq.property.linked-references" "logseq.property.asset" "logseq.property.table" "logseq.property.node"
@@ -496,6 +490,11 @@
   [s]
   [s]
   (string/includes? s ".property"))
   (string/includes? s ".property"))
 
 
+(defn user-class-namespace?
+  "Determines if namespace string is a user class"
+  [s]
+  (string/includes? s ".class"))
+
 (defn property?
 (defn property?
   "Determines if ident kw is a property visible to user"
   "Determines if ident kw is a property visible to user"
   [k]
   [k]
@@ -503,6 +502,7 @@
     (and k-name
     (and k-name
          (or (contains? logseq-property-namespaces k-name)
          (or (contains? logseq-property-namespaces k-name)
              (user-property-namespace? k-name)
              (user-property-namespace? k-name)
+             (user-class-namespace? k-name)
              ;; disallow private db-attribute-properties as they cause unwanted refs
              ;; disallow private db-attribute-properties as they cause unwanted refs
              ;; and appear noisily in debugging contexts
              ;; and appear noisily in debugging contexts
              (and (keyword? k) (contains? public-db-attribute-properties k))))))
              (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
 (defn- closed-value-new-block
   [block-id block-type value property]
   [block-id block-type value property]
   (let [property-id (:db/ident property)]
   (let [property-id (:db/ident property)]
-    (merge {:block/type "closed value"
-            :block/format :markdown
+    (merge {:block/format :markdown
             :block/uuid block-id
             :block/uuid block-id
             :block/page property-id
             :block/page property-id
             :block/closed-value-property 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]
   [db val]
   (when-let [ent (d/entity db val)]
   (when-let [ent (d/entity db val)]
     (and (some? (:block/title ent))
     (and (some? (:block/title ent))
-         (= (:block/type ent) "journal"))))
+         (entity-util/journal? ent))))
 
 
 (def built-in-validation-schemas
 (def built-in-validation-schemas
   "Map of types to malli validation schemas that validate a property value for that type"
   "Map of types to malli validation schemas that validate a property value for that type"
@@ -167,10 +167,18 @@
 
 
    :string   string?
    :string   string?
    :raw-number number?
    :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?
    :keyword  keyword?
    :map      map?
    :map      map?
    ;; coll elements are ordered as it's saved as a vec
    ;; 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
    (dissoc query-dsl-rules :namespace
            :page-property :has-page-property
            :page-property :has-page-property
            :page-tags :all-page-tags)
            :page-tags :all-page-tags)
+
    (dissoc rules :namespace)
    (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
     '[;; non-ref value
       [(existing-property-value ?b ?prop ?val)
       [(existing-property-value ?b ?prop ?val)
        [?prop-e :db/ident ?prop]
        [?prop-e :db/ident ?prop]
@@ -229,7 +239,7 @@
     :has-simple-query-property
     :has-simple-query-property
     '[(has-simple-query-property ?b ?prop)
     '[(has-simple-query-property ?b ?prop)
       [?prop-e :db/ident ?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)
       (has-property-or-default-value? ?b ?prop)
       [?prop-e :block/schema ?prop-schema]
       [?prop-e :block/schema ?prop-schema]
       [(get ?prop-schema :public? true) ?public]
       [(get ?prop-schema :public? true) ?public]
@@ -239,7 +249,7 @@
     :has-private-simple-query-property
     :has-private-simple-query-property
     '[(has-private-simple-query-property ?b ?prop)
     '[(has-private-simple-query-property ?b ?prop)
       [?prop-e :db/ident ?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)]
       (has-property-or-default-value? ?b ?prop)]
 
 
     ;; Checks if a property exists for any features that are not simple queries
     ;; Checks if a property exists for any features that are not simple queries
@@ -247,7 +257,7 @@
     '[(has-property ?b ?prop)
     '[(has-property ?b ?prop)
       [?b ?prop _]
       [?b ?prop _]
       [?prop-e :db/ident ?prop]
       [?prop-e :db/ident ?prop]
-      [?prop-e :block/type "property"]
+      [?prop-e :block/tags :logseq.class/Property]
       [?prop-e :block/schema ?prop-schema]
       [?prop-e :block/schema ?prop-schema]
       [(get ?prop-schema :public? true) ?public]
       [(get ?prop-schema :public? true) ?public]
       [(= true ?public)]]
       [(= true ?public)]]
@@ -256,7 +266,7 @@
     :property
     :property
     '[(property ?b ?prop ?val)
     '[(property ?b ?prop ?val)
       [?prop-e :db/ident ?prop]
       [?prop-e :db/ident ?prop]
-      [?prop-e :block/type "property"]
+      [?prop-e :block/tags :logseq.class/Property]
       [?prop-e :block/schema ?prop-schema]
       [?prop-e :block/schema ?prop-schema]
       [(get ?prop-schema :public? true) ?public]
       [(get ?prop-schema :public? true) ?public]
       [(= true ?public)]
       [(= true ?public)]
@@ -276,7 +286,7 @@
     :simple-query-property
     :simple-query-property
     '[(simple-query-property ?b ?prop ?val)
     '[(simple-query-property ?b ?prop ?val)
       [?prop-e :db/ident ?prop]
       [?prop-e :db/ident ?prop]
-      [?prop-e :block/type "property"]
+      [?prop-e :block/tags :logseq.class/Property]
       [?prop-e :block/schema ?prop-schema]
       [?prop-e :block/schema ?prop-schema]
       [(get ?prop-schema :public? true) ?public]
       [(get ?prop-schema :public? true) ?public]
       [(get ?prop-schema :type) ?type]
       [(get ?prop-schema :type) ?type]
@@ -287,7 +297,7 @@
     :private-simple-query-property
     :private-simple-query-property
     '[(private-simple-query-property ?b ?prop ?val)
     '[(private-simple-query-property ?b ?prop ?val)
       [?prop-e :db/ident ?prop]
       [?prop-e :db/ident ?prop]
-      [?prop-e :block/type "property"]
+      [?prop-e :block/tags :logseq.class/Property]
       (property-value ?b ?prop-e ?val)]
       (property-value ?b ?prop-e ?val)]
 
 
     :tags
     :tags

+ 3 - 2
deps/db/src/logseq/db/frontend/schema.cljs

@@ -2,7 +2,7 @@
   "Main datascript schemas for the Logseq app"
   "Main datascript schemas for the Logseq app"
   (:require [clojure.set :as set]))
   (: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".
 ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
 (def ^:large-vars/data-var schema
 (def ^:large-vars/data-var schema
@@ -111,7 +111,8 @@
    (dissoc schema
    (dissoc schema
            :block/namespace :block/properties-text-values :block/pre-block? :recent/pages :block/file
            :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/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
    {:block/name {:db/index true}        ; remove db/unique for :block/name
     ;; closed value
     ;; closed value
     :block/closed-value-property {:db/valueType :db.type/ref
     :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?]
   [closed-schema?]
   (if closed-schema? closed-db-schema-explainer db-schema-explainer))
   (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!
 (defn validate-tx-report!
   "Validates the datascript tx-report for entities that have changed. Returns
   "Validates the datascript tx-report for entities that have changed. Returns
   boolean indicating if db is valid"
   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)
   (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)
         tx-datoms (mapcat #(d/datoms db-after :eavt %) changed-ids)
         ent-maps* (map (fn [[db-id m]]
         ent-maps* (map (fn [[db-id m]]
                          ;; Add :db/id for debugging
                          ;; Add :db/id for debugging
@@ -62,13 +41,8 @@
           (let [explainer (get-schema-explainer (:closed-schema? validate-options))]
           (let [explainer (get-schema-explainer (:closed-schema? validate-options))]
             (js/console.error "Invalid datascript entities detected amongst changed entity ids:" changed-ids)
             (js/console.error "Invalid datascript entities detected amongst changed entity ids:" changed-ids)
             (doseq [m invalid-ent-maps]
             (doseq [m invalid-ent-maps]
-
               (prn {:entity-map m
               (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)
             false)
           true)))))
           true)))))
 
 
@@ -88,7 +62,7 @@
                            (:block/page ent)
                            (:block/page ent)
                            (update :block/page
                            (update :block/page
                                    (fn [id] (select-keys (d/entity db id)
                                    (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'
                :errors errors'
                ;; Group by type to reduce verbosity
                ;; Group by type to reduce verbosity
                ;; TODO: Move/remove this to another fn if unused
                ;; 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))
                       {:db/id (or (:db/id page) (new-db-id))
                        :block/title (or (:block/title page) (string/capitalize (:block/name page)))
                        :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/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}
                        :block/format :markdown}
                       (dissoc page :build/properties :db/id :block/name :block/title :build/tags))
                       (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)]
             pvalue-tx-m (->property-value-tx-m new-page (:build/properties page) properties all-idents)]
@@ -376,8 +376,9 @@
                                     page-uuids
                                     page-uuids
                                     all-idents))
                                     all-idents))
               (when-let [tags (:build/tags page)]
               (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
          ;; blocks tx
          (reduce (fn [acc m]
          (reduce (fn [acc m]
                    (into acc
                    (into acc
@@ -480,7 +481,6 @@
                                                    :block/title page-name
                                                    :block/title page-name
                                                    :block/uuid
                                                    :block/uuid
                                                    (common-uuid/gen-uuid :journal-page-uuid date-int)
                                                    (common-uuid/gen-uuid :journal-page-uuid date-int)
-                                                   :block/type "journal"
                                                    :block/tags :logseq.class/Journal})))))
                                                    :block/tags :logseq.class/Journal})))))
                            m))]
                            m))]
     ;; Order matters as some steps depend on previous step having prepared blocks or pages in a certain way
     ;; 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
             :in $ ?today
             :where
             :where
             [?page :block/name ?page-name]
             [?page :block/name ?page-name]
-            ;; [?page :block/type "journal"]
             [?page :block/journal-day ?journal-day]
             [?page :block/journal-day ?journal-day]
             [(<= ?journal-day ?today)]]
             [(<= ?journal-day ?today)]]
           db
           db
@@ -213,13 +212,9 @@
 
 
 (defn get-structured-datoms
 (defn get-structured-datoms
   [db]
   [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
 (defn get-favorites
   "Favorites page and its blocks"
   "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.frontend.schema :as db-schema]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.common.config :as common-config]
             [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]
 (defn- mark-block-as-built-in [block]
   (assoc block :logseq.property/built-in? true))
   (assoc block :logseq.property/built-in? true))
@@ -52,14 +53,15 @@
                    ;; Adding built-ins must come after initial properties
                    ;; Adding built-ins must come after initial properties
                    [(mark-block-as-built-in' built-in-property)]
                    [(mark-block-as-built-in' built-in-property)]
                    (map mark-block-as-built-in' properties)
                    (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))]
                          properties))]
     (doseq [m tx]
     (doseq [m tx]
       (when-let [block-uuid (and (:db/ident m) (:block/uuid m))]
       (when-let [block-uuid (and (:db/ident m) (:block/uuid m))]
         (assert (string/starts-with? (str block-uuid) "00000002") m)))
         (assert (string/starts-with? (str block-uuid) "00000002") m)))
 
 
     {:tx tx
     {:tx tx
-     :properties (filter #(= (:block/type %) "property") properties)}))
+     :properties (filter entity-util/property? properties)}))
 
 
 (def built-in-pages-names
 (def built-in-pages-names
   #{"Contents"})
   #{"Contents"})
@@ -69,7 +71,6 @@
              (->> (keep :db/ident tx)
              (->> (keep :db/ident tx)
                   frequencies
                   frequencies
                   (keep (fn [[k v]] (when (> v 1) k)))
                   (keep (fn [[k v]] (when (> v 1) k)))
-                  (remove #{:logseq.class/Root})
                   seq)]
                   seq)]
     (throw (ex-info (str "The following :db/idents are not unique and clobbered each other: "
     (throw (ex-info (str "The following :db/idents are not unique and clobbered each other: "
                          (vec conflicting-idents))
                          (vec conflicting-idents))
@@ -111,7 +112,7 @@
       {:block/uuid page-id
       {:block/uuid page-id
        :block/name common-config/views-page-name
        :block/name common-config/views-page-name
        :block/title 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/schema {:public? false}
        :block/format :markdown
        :block/format :markdown
        :logseq.property/built-in? true})
        :logseq.property/built-in? true})
@@ -131,7 +132,7 @@
     {:block/uuid (common-uuid/gen-uuid)
     {:block/uuid (common-uuid/gen-uuid)
      :block/name common-config/favorites-page-name
      :block/name common-config/favorites-page-name
      :block/title 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/schema {:public? false}
      :block/format :markdown
      :block/format :markdown
      :logseq.property/built-in? true})])
      :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-initial-schema-version db-schema/version)
                        (sqlite-util/kv :logseq.kv/graph-created-at (common-util/time-ms))
                        (sqlite-util/kv :logseq.kv/graph-created-at (common-util/time-ms))
                        ;; Empty property value used by db.type/ref properties
                        ;; 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
                        import-type
                        (into (sqlite-util/import-tx import-type)))
                        (into (sqlite-util/import-tx import-type)))
         initial-files [{:block/uuid (d/squuid)
         initial-files [{:block/uuid (d/squuid)
@@ -172,7 +172,15 @@
         default-pages (->> (map sqlite-util/build-new-page built-in-pages-names)
         default-pages (->> (map sqlite-util/build-new-page built-in-pages-names)
                            (map mark-block-as-built-in))
                            (map mark-block-as-built-in))
         hidden-pages (concat (build-initial-views) (build-favorites-page))
         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))]
                         initial-files default-pages hidden-pages))]
     (validate-tx-for-duplicate-idents tx)
     (validate-tx-for-duplicate-idents tx)
     tx))
     tx))

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

@@ -91,7 +91,7 @@
      (block-with-timestamps
      (block-with-timestamps
       (cond->
       (cond->
        {:db/ident db-ident'
        {:db/ident db-ident'
-        :block/type "property"
+        :block/tags #{:logseq.class/Property}
         :block/format :markdown
         :block/format :markdown
         :block/schema (merge {:type :default} (dissoc prop-schema :classes :cardinality))
         :block/schema (merge {:type :default} (dissoc prop-schema :classes :cardinality))
         :block/name (common-util/page-name-sanity-lc (name prop-name))
         :block/name (common-util/page-name-sanity-lc (name prop-name))
@@ -115,8 +115,8 @@
   {:pre [(qualified-keyword? (:db/ident block))]}
   {:pre [(qualified-keyword? (:db/ident block))]}
   (block-with-timestamps
   (block-with-timestamps
    (cond-> (merge block
    (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)
      (and (not= (:db/ident block) :logseq.class/Root)
           (nil? (:logseq.property/parent block)))
           (nil? (:logseq.property/parent block)))
      (assoc :logseq.property/parent :logseq.class/Root))))
      (assoc :logseq.property/parent :logseq.class/Root))))
@@ -129,7 +129,7 @@
     :block/title page-name
     :block/title page-name
     :block/uuid (d/squuid)
     :block/uuid (d/squuid)
     :block/format :markdown
     :block/format :markdown
-    :block/type "page"}))
+    :block/tags #{:logseq.class/Page}}))
 
 
 (defn kv
 (defn kv
   "Creates a key-value pair tx with the key and value respectively stored under
   "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.sqlite.create-graph :as sqlite-create-graph]
             [logseq.db.frontend.schema :as db-schema]))
             [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)
             db)
-       first))
+       first
+       (d/entity db)))
 
 
 (defn create-conn
 (defn create-conn
   "Create a conn for a DB graph seeded with initial data"
   "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
 (deftest resolve-input-for-page-and-block-inputs
   (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
   (let [conn (d/create-conn db-schema/schema-for-db-based-graph)
+        _ (d/transact! conn [{:db/ident :logseq.class/Page}])
         _ (sqlite-build/create-blocks
         _ (sqlite-build/create-blocks
            conn
            conn
            [{:page {:block/title "page1"}
            [{: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}}
               {:properties {:foo {:block/schema {:type :default}}
                             :foo2 {:block/schema {:type :default}}}
                             :foo2 {:block/schema {:type :default}}}
                :pages-and-blocks
                :pages-and-blocks
-               [{:page {:block/title "Page"
+               [{:page {:block/title "Page1"
                         :build/properties {:foo "bar"}}}]})]
                         :build/properties {:foo "bar"}}}]})]
 
 
-    (is (= ["Page"]
+    (is (= ["Page1"]
            (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (has-property ?b :user.property/foo)]
            (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (has-property ?b :user.property/foo)]
                               @conn)
                               @conn)
                 (map (comp :block/title first))))
                 (map (comp :block/title first))))
@@ -44,9 +44,9 @@
                               @conn)
                               @conn)
                 (map (comp :block/title first))))
                 (map (comp :block/title first))))
         "has-property returns no result when block doesn't have property")
         "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 ...]
            (q-with-rules '[:find [?p ...]
-                           :where (has-property ?b ?p) [?b :block/title "Page"]]
+                           :where (has-property ?b ?p) [?b :block/title "Page1"]]
                          @conn))
                          @conn))
         "has-property can bind to property arg")))
         "has-property can bind to property arg")))
 
 
@@ -57,12 +57,12 @@
                             :number-many {:block/schema {:type :number :cardinality :many}}
                             :number-many {:block/schema {:type :number :cardinality :many}}
                             :page-many {:block/schema {:type :node :cardinality :many}}}
                             :page-many {:block/schema {:type :node :cardinality :many}}}
                :pages-and-blocks
                :pages-and-blocks
-               [{:page {:block/title "Page"
+               [{:page {:block/title "Page1"
                         :build/properties {:foo "bar" :number-many #{5 10} :page-many #{[:page "Page A"]}}}}
                         :build/properties {:foo "bar" :number-many #{5 10} :page-many #{[:page "Page A"]}}}}
                 {:page {:block/title "Page A"
                 {:page {:block/title "Page A"
                         :build/properties {:foo "bar A"}}}]})]
                         :build/properties {:foo "bar A"}}}]})]
     (testing "cardinality :one property"
     (testing "cardinality :one property"
-      (is (= ["Page"]
+      (is (= ["Page1"]
              (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/foo "bar")]
              (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/foo "bar")]
                                 @conn)
                                 @conn)
                   (map (comp :block/title first))))
                   (map (comp :block/title first))))
@@ -74,13 +74,13 @@
           "property returns no result when page doesn't have property value")
           "property returns no result when page doesn't have property value")
       (is (= #{:user.property/foo}
       (is (= #{:user.property/foo}
              (->> (q-with-rules '[:find [?p ...]
              (->> (q-with-rules '[:find [?p ...]
-                                  :where (property ?b ?p "bar") [?b :block/title "Page"]]
+                                  :where (property ?b ?p "bar") [?b :block/title "Page1"]]
                                 @conn)
                                 @conn)
                   set))
                   set))
           "property can bind to property arg with bound property value"))
           "property can bind to property arg with bound property value"))
 
 
     (testing "cardinality :many property"
     (testing "cardinality :many property"
-      (is (= ["Page"]
+      (is (= ["Page1"]
              (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/number-many 5)]
              (->> (q-with-rules '[:find (pull ?b [:block/title]) :where (property ?b :user.property/number-many 5)]
                                 @conn)
                                 @conn)
                   (map (comp :block/title first))))
                   (map (comp :block/title first))))
@@ -92,14 +92,14 @@
           "property returns no result when page doesn't have property value")
           "property returns no result when page doesn't have property value")
       (is (= #{:user.property/number-many}
       (is (= #{:user.property/number-many}
              (->> (q-with-rules '[:find [?p ...]
              (->> (q-with-rules '[:find [?p ...]
-                                  :where (property ?b ?p 5) [?b :block/title "Page"]]
+                                  :where (property ?b ?p 5) [?b :block/title "Page1"]]
                                 @conn)
                                 @conn)
                   set))
                   set))
           "property can bind to property arg with bound property value"))
           "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
     ;; NOTE: Querying a ref's name is different than before and requires more than just the rule
     (testing ":ref property"
     (testing ":ref property"
-      (is (= ["Page"]
+      (is (= ["Page1"]
              (->> (q-with-rules '[:find (pull ?b [:block/title])
              (->> (q-with-rules '[:find (pull ?b [:block/title])
                                   :where (property ?b :user.property/page-many "Page A")]
                                   :where (property ?b :user.property/page-many "Page A")]
                                 @conn)
                                 @conn)
@@ -113,22 +113,23 @@
           "property returns no result when page doesn't have property value"))
           "property returns no result when page doesn't have property value"))
 
 
     (testing "bindings with 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 ...]
              (->> (q-with-rules '[:find [?p ...]
-                                  :where (property ?b ?p _) [?b :block/title "Page"]]
+                                  :where (property ?b ?p _) [?b :block/title "Page1"]]
                                 @conn)
                                 @conn)
                   set))
                   set))
           "property can bind to property arg with unbound property value")
           "property can bind to property arg with unbound property value")
       (is (= #{[:user.property/number-many 10]
       (is (= #{[:user.property/number-many 10]
                [:user.property/number-many 5]
                [:user.property/number-many 5]
                [:user.property/foo "bar"]
                [: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
              (->> (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)
                                 @conn)
                   set))
                   set))
           "property can bind to property and property value args")
           "property can bind to property and property value args")
-      (is (= #{"Page"}
+      (is (= #{"Page1"}
              (->> (q-with-rules '[:find (pull ?b [:block/title])
              (->> (q-with-rules '[:find (pull ?b [:block/title])
                                   :where
                                   :where
                                   [?b :user.property/page-many ?pv]
                                   [?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"}
            [{:page {:block/title "page1"}
              :blocks [{:block/title "Jrue Holiday" :build/tags [:Person]}]}
              :blocks [{:block/title "Jrue Holiday" :build/tags [:Person]}]}
             {:page {:block/title "Jayson Tatum" :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"]]
                          :where [?b :block/title "Jrue Holiday"]]
                        @conn)))
                        @conn)))
         "Person class is created and correctly associated to a block")
         "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"]]
                          :where [?b :block/title "Jayson Tatum"]]
                        @conn)))
                        @conn)))
         "Person class is created and correctly associated to a page")))
         "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.frontend.property :as db-property]
             [logseq.db.sqlite.build :as sqlite-build]
             [logseq.db.sqlite.build :as sqlite-build]
             [logseq.db :as ldb]
             [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
 (deftest new-graph-db-idents
   (testing "a new graph follows :db/ident conventions for"
   (testing "a new graph follows :db/ident conventions for"
     (let [conn (db-test/create-conn)
     (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]]
                                  :where [?b :db/ident]]
                                @conn)
                                @conn)
-                          (map first))
+                          (map (fn [id] (d/entity @conn id))))
           default-idents (map :db/ident ident-ents)]
           default-idents (map :db/ident ident-ents)]
       (is (> (count default-idents) 45)
       (is (> (count default-idents) 45)
           "Approximate number of default idents is correct")
           "Approximate number of default idents is correct")
@@ -38,7 +39,7 @@
                                            (map #(keyword (namespace %) (string/replace (name %) #".[^.]+$" "")))
                                            (map #(keyword (namespace %) (string/replace (name %) #".[^.]+$" "")))
                                            set)]
                                            set)]
           (is (= []
           (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")
               "All property names that contain a '.' are closed values")
           (is (= #{}
           (is (= #{}
                  (set/difference
                  (set/difference
@@ -56,7 +57,7 @@
                     (remove #(or (= "logseq.kv" (namespace (:db/ident %)))
                     (remove #(or (= "logseq.kv" (namespace (:db/ident %)))
                                  (= :logseq.property/empty-placeholder (:db/ident %)))))
                                  (= :logseq.property/empty-placeholder (:db/ident %)))))
         pages (d/q '[:find [(pull ?b [:logseq.property/built-in? :block/title]) ...]
         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)]
                    @conn)]
     (is (= [] (remove :logseq.property/built-in? idents))
     (is (= [] (remove :logseq.property/built-in? idents))
         "All entities with :db/ident have built-in property (except for kv 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))
     (is (every? ldb/property? (:logseq.property.class/properties task))
         "Each task property has correct type")))
         "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
 (deftest new-graph-is-valid
   (let [conn (db-test/create-conn)
   (let [conn (db-test/create-conn)
         validation (db-validate/validate-db! @conn)]
         validation (db-validate/validate-db! @conn)]

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

@@ -29,16 +29,16 @@
                   (ex-message e)))))))
                   (ex-message e)))))))
 
 
 (def class-parents-data
 (def class-parents-data
-  [{:block/type "class"
+  [{:block/tags :logseq.class/Tag
     :block/title "x"
     :block/title "x"
     :block/name "x"
     :block/name "x"
     :block/uuid #uuid "6c353967-f79b-4785-b804-a39b81d72461"}
     :block/uuid #uuid "6c353967-f79b-4785-b804-a39b81d72461"}
-   {:block/type "class"
+   {:block/tags :logseq.class/Tag
     :block/title "y"
     :block/title "y"
     :block/name "y"
     :block/name "y"
     :block/uuid #uuid "7008db08-ba0c-4aa9-afc6-7e4783e40a99"
     :block/uuid #uuid "7008db08-ba0c-4aa9-afc6-7e4783e40a99"
     :logseq.property/parent [:block/uuid #uuid "6c353967-f79b-4785-b804-a39b81d72461"]}
     :logseq.property/parent [:block/uuid #uuid "6c353967-f79b-4785-b804-a39b81d72461"]}
-   {:block/type "class"
+   {:block/tags :logseq.class/Tag
     :block/title "z"
     :block/title "z"
     :block/name "z"
     :block/name "z"
     :block/uuid #uuid "d95f2912-a7af-41b9-8ed5-28861f7fc0be"
     :block/uuid #uuid "d95f2912-a7af-41b9-8ed5-28861f7fc0be"
@@ -63,4 +63,23 @@
     (is (= "Foo" (:block/title (ldb/get-case-page @conn "Foo"))))
     (is (= "Foo" (:block/title (ldb/get-case-page @conn "Foo"))))
     ;; Case sensitive classes
     ;; 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"))))))
+    (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/created-at current-ms
                    :block/updated-at current-ms}))
                    :block/updated-at current-ms}))
               (if journal-day
               (if journal-day
-                (cond-> {:block/type "journal"
-                         :block/journal-day journal-day}
+                (cond-> {:block/journal-day journal-day}
                   db-based?
                   db-based?
-                  (assoc :block/tags [:logseq.class/Journal]))
+                  (assoc :block/tags [:logseq.class/Journal])
+                  (not db-based?)
+                  (assoc :block/type "journal"))
                 {}))]
                 {}))]
     [page page-entity]))
     [page page-entity]))
 
 
@@ -388,8 +389,12 @@
                                                  nil)]
                                                  nil)]
                                   [page nil]))]
                                   [page nil]))]
       (when page
       (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?
 (defn- db-namespace-page?
   "Namespace page that're not journal pages"
   "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]
   ([db class-name all-idents]
    (find-or-create-class db class-name all-idents {}))
    (find-or-create-class db class-name all-idents {}))
   ([db class-name all-idents class-block]
   ([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?]}]
 (defn- find-or-gen-class-uuid [page-names-to-uuids page-name db-ident & {:keys [temp-new-class?]}]
   (or (if temp-new-class?
   (or (if temp-new-class?
@@ -115,7 +118,7 @@
   (if block-ns
   (if block-ns
     (->> (d/q '[:find [?b ...]
     (->> (d/q '[:find [?b ...]
                 :in $ ?name
                 :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
               db
               (ns-util/get-last-part full-name))
               (ns-util/get-last-part full-name))
          (map #(d/entity db %))
          (map #(d/entity db %))
@@ -127,7 +130,7 @@
     (first
     (first
      (d/q '[:find [?uuid ...]
      (d/q '[:find [?uuid ...]
             :in $ ?name
             :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
           db
           full-name))))
           full-name))))
 
 
@@ -144,21 +147,30 @@
       (assert (:block/uuid class-m') "Class must have a :block/uuid")
       (assert (:block/uuid class-m') "Class must have a :block/uuid")
       [:block/uuid (:block/uuid class-m')])
       [:block/uuid (:block/uuid class-m')])
     (when (convert-tag? (:block/name tag-block) user-options)
     (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?
 (defn- logseq-class-ident?
   [k]
   [k]
@@ -188,10 +200,17 @@
                                       ;; Ignore new class tags from extract e.g. :logseq.class/Journal
                                       ;; Ignore new class tags from extract e.g. :logseq.class/Journal
                                       (logseq-class-ident? %)))
                                       (logseq-class-ident? %)))
                          (map #(vector :block/uuid (get-page-uuid (:page-names-to-uuids per-file-state) (:block/name %) {:block %})))
                          (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
       (cond-> block
         true
         true
         (update :block/tags convert-tags-to-classes db per-file-state user-options all-idents)
         (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)
         (seq page-tags)
         (merge {:logseq.property/page-tags page-tags})))
         (merge {:logseq.property/page-tags page-tags})))
     block))
     block))
@@ -297,9 +316,8 @@
                                            (date-time-util/int->journal-title date-int (common-config/get-date-formatter user-config)))]
                                            (date-time-util/int->journal-title date-int (common-config/get-date-formatter user-config)))]
                                (assoc page-m
                                (assoc page-m
                                       :block/uuid (common-uuid/gen-uuid :journal-page-uuid date-int)
                                       :block/uuid (common-uuid/gen-uuid :journal-page-uuid date-int)
-                                      :block/type "journal"
                                       :block/journal-day date-int)))
                                       :block/journal-day date-int)))
-                         (assoc :block/tags :logseq.class/Journal))]
+                         (assoc :block/tags #{:logseq.class/Journal}))]
       {:block
       {:block
        (-> block
        (-> block
            (assoc :logseq.task/deadline [:block/uuid (:block/uuid deadline-page)])
            (assoc :logseq.task/deadline [:block/uuid (:block/uuid deadline-page)])
@@ -699,8 +717,9 @@
              (seq classes-from-properties)
              (seq classes-from-properties)
              ;; Add a map of {:block.temp/new-class TAG} to be processed later
              ;; Add a map of {:block.temp/new-class TAG} to be processed later
              (update :block/tags
              (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})
            :properties-tx pvalues-tx})
         {:block block :properties-tx []})
         {:block block :properties-tx []})
       (update :block dissoc :block/properties :block/properties-text-values :block/properties-order :block/invalid-properties)))
       (update :block dissoc :block/properties :block/properties-text-values :block/properties-order :block/invalid-properties)))
@@ -911,7 +930,7 @@
                 (:block/name %))
                 (:block/name %))
               (or (:block/uuid %)
               (or (:block/uuid %)
                   (throw (ex-info (str "No uuid for existing page " (pr-str (:block/name %)))
                   (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 {})))
        (into {})))
 
 
 (defn- build-existing-page
 (defn- build-existing-page
@@ -919,13 +938,10 @@
   (let [;; These attributes are not allowed to be transacted because they must not change across files
   (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
         disallowed-attributes [:block/name :block/uuid :block/format :block/title :block/journal-day
                                :block/created-at :block/updated-at]
                                :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 %))
                                  (keep #(when (db-malli-schema/user-property? (key %)) (key %))
                                        m))
                                        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)))]
     (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)) ": "
       (notify-user {:msg (str "Import ignored the following attributes on page " (pr-str (:block/title m)) ": "
                               ignored-attrs)}))
                               ignored-attrs)}))
@@ -956,8 +972,8 @@
             (assoc :block/uuid (d/squuid))
             (assoc :block/uuid (d/squuid))
             ;; only happens for few file built-ins like tags and alias
             ;; only happens for few file built-ins like tags and alias
             (and (contains? all-built-in-names (keyword (:block/name page)))
             (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'
     (cond-> page'
       (:block/namespace page)
       (:block/namespace page)
       ((fn [block']
       ((fn [block']
@@ -997,7 +1013,7 @@
                                                          (all-existing-page-uuids (::original-name m))
                                                          (all-existing-page-uuids (::original-name m))
                                                          (all-existing-page-uuids (:block/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)
                                       (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
                                                 ;; Don't build a new page if it overwrites an existing class
                                                 (not (some-> (get @(:all-idents import-state)
                                                 (not (some-> (get @(:all-idents import-state)
                                                                   (some-> (or (::original-title m) (:block/title m))
                                                                   (some-> (or (::original-title m) (:block/title m))
@@ -1137,18 +1153,22 @@
                                                               (get-property-schema @(:property-schemas import-state) kw-name)
                                                               (get-property-schema @(:property-schemas import-state) kw-name)
                                                               {:title (name kw-name)})]
                                                               {:title (name kw-name)})]
                  (assert existing-page-uuid)
                  (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})))
                         {:block/uuid existing-page-uuid})))
              (set/intersection new-properties (set (map keyword (keys existing-pages)))))
              (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
         ;; Save properties on new property pages separately as they can contain new properties and thus need to be
         ;; transacted separately the property pages
         ;; transacted separately the property pages
         property-page-properties-tx (keep (fn [b]
         property-page-properties-tx (keep (fn [b]
                                             (when-let [page-properties (not-empty (db-property/properties b))]
                                             (when-let [page-properties (not-empty (db-property/properties b))]
                                               (merge page-properties {:block/uuid (:block/uuid 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)]
                                           properties-tx)]
     {:pages-tx pages-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}))
      :property-page-properties-tx property-page-properties-tx}))
 
 
 (defn- update-whiteboard-blocks [blocks format]
 (defn- update-whiteboard-blocks [blocks format]
@@ -1219,8 +1239,9 @@
                                (->> pages
                                (->> pages
                                     ;; migrate previous attribute for :block/title
                                     ;; migrate previous attribute for :block/title
                                     (map #(-> %
                                     (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))
               (update :blocks update-whiteboard-blocks format))
 
 
           :else
           :else
@@ -1234,6 +1255,32 @@
                [(:block/name %) (date-time-util/journal-day->ms journal-day)]))
                [(:block/name %) (date-time-util/journal-day->ms journal-day)]))
        (into {})))
        (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
 (defn add-file-to-db-graph
   "Parse file and save parsed data to the given db graph. Options available:
   "Parse file and save parsed data to the given db graph. Options available:
 
 
@@ -1271,12 +1318,15 @@
                        vec)
                        vec)
         {:keys [property-pages-tx property-page-properties-tx] pages-tx' :pages-tx}
         {: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))
         (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
         ;; 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})
         main-props-tx-report (d/transact! conn property-pages-tx {::new-graph? true})
 
 
         classes-tx @(:classes-tx tx-options)
         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
         ;; 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))
                          (concat (map #(select-keys % [:block/uuid]) classes-tx))
                          distinct)
                          distinct)
         block-ids (map (fn [block] {:block/uuid (:block/uuid block)}) blocks-tx)
         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))
         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
         ;; 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
         ;; 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)
         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)))
         ;; _ (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'}))
         ;; _ (when (not (seq whiteboard-pages)) (cljs.pprint/pprint {#_:property-pages-tx #_property-pages-tx :tx tx'}))
@@ -1297,6 +1347,7 @@
 
 
         upstream-properties-tx
         upstream-properties-tx
         (build-upstream-properties-tx @conn @(:upstream-properties tx-options) (:import-state options) log-fn)
         (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}))]
         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
     ;; Return all tx-reports that occurred in this fn as UI needs to know what changed
@@ -1392,7 +1443,7 @@
 (defn- export-class-properties
 (defn- export-class-properties
   [conn repo-or-conn]
   [conn repo-or-conn]
   (let [user-classes (->> (d/q '[:find (pull ?b [:db/id :db/ident])
   (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)
                           (map first)
                           (remove #(db-class/built-in-classes (:db/ident %))))
                           (remove #(db-class/built-in-classes (:db/ident %))))
         class-to-prop-uuids
         class-to-prop-uuids
@@ -1404,7 +1455,7 @@
                     [(contains? ?user-classes ?class)]
                     [(contains? ?user-classes ?class)]
                     [?b ?prop _]
                     [?b ?prop _]
                     [?prop-e :db/ident ?prop]
                     [?prop-e :db/ident ?prop]
-                    [?prop-e :block/type "property"]]
+                    [?prop-e :block/tags :logseq.class/Property]]
                   @conn
                   @conn
                   (set (map :db/ident user-classes)))
                   (set (map :db/ident user-classes)))
              (remove #(ldb/built-in? (d/entity @conn (second %))))
              (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"
     (sh ["git" "clone" "--depth" "1" "-b" branch "-c" "advice.detachedHead=false"
          "https://github.com/logseq/docs" dir] {})))
          "https://github.com/logseq/docs" dir] {})))
 
 
-
 ;; Fns for common test assertions
 ;; Fns for common test assertions
 ;; ==============================
 ;; ==============================
 (defn get-top-block-properties
 (defn get-top-block-properties
@@ -62,7 +61,7 @@
 (defn- get-journal-page-count [db]
 (defn- get-journal-page-count [db]
   (->> (d/q '[:find (count ?b)
   (->> (d/q '[:find (count ?b)
               :where
               :where
-              [?b :block/type "journal"]
+              [?b :block/journal-day]
               [?b :block/name]
               [?b :block/name]
               [?b :block/file]]
               [?b :block/file]]
             db)
             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
 ;; 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
 (defn- extract-rules
   [rules]
   [rules]
   (rules/extract-rules rules/db-query-dsl-rules
   (rules/extract-rules rules/db-query-dsl-rules
@@ -63,14 +46,6 @@
             db property property-value (extract-rules [:property]))
             db property property-value (extract-rules [:property]))
        first))
        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
 (defn- build-graph-files
   "Given a file graph directory, return all files including assets and adds relative paths
   "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
    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))))
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
         "Created graph has no validation errors")
         "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
 (deftest-async export-basic-graph-with-convert-all-tags
   ;; This graph will contain basic examples of different features to import
   ;; This graph will contain basic examples of different features to import
@@ -195,25 +177,30 @@
 
 
       ;; Counts
       ;; Counts
       ;; Includes journals as property values e.g. :logseq.task/deadline
       ;; 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 (= 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 (= 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 (= 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))))
       (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
       (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")
           "Correct number of pages with block content")
       (is (= 11 (->> @conn
       (is (= 11 (->> @conn
                      (d/q '[:find [?ident ...]
                      (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))
                      count))
           "Correct number of user classes")
           "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 (= 0 (count @(:ignored-properties import-state))) ":filters should be the only ignored property")
       (is (= 1 (count @assets))))
       (is (= 1 (count @assets))))
 
 
@@ -236,7 +223,7 @@
       (is (= 18
       (is (= 18
              (->> @conn
              (->> @conn
                   (d/q '[:find [(pull ?b [:db/ident]) ...]
                   (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 %)))
                   (remove #(db-malli-schema/internal-ident? (:db/ident %)))
                   count))
                   count))
           "Correct number of user properties")
           "Correct number of user properties")
@@ -248,7 +235,7 @@
                {:db/ident :user.property/startedat :block/schema {:type :date}}}
                {:db/ident :user.property/startedat :block/schema {:type :date}}}
              (->> @conn
              (->> @conn
                   (d/q '[:find [(pull ?b [:db/ident :block/schema]) ...]
                   (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}
                   (filter #(contains? #{:prop-bool :prop-string :prop-num :rangeincludes :sameas :startedat}
                                       (keyword (name (:db/ident %)))))
                                       (keyword (name (:db/ident %)))))
                   set))
                   set))
@@ -263,45 +250,55 @@
       (is (= {:user.property/prop-bool true
       (is (= {:user.property/prop-bool true
               :user.property/prop-num 5
               :user.property/prop-num 5
               :user.property/prop-string "woot"}
               :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))))
                           (fn [v] (if (map? v) (db-property/ref->property-value-content @conn v) v))))
           "Basic block has correct properties")
           "Basic block has correct properties")
       (is (= #{"prop-num" "prop-string" "prop-bool"}
       (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
                   :block/refs
                   (map :block/title)
                   (map :block/title)
                   set))
                   set))
           "Block with properties has correct refs")
           "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")
           "New page has correct properties")
       (is (= {:user.property/prop-bool true
       (is (= {:user.property/prop-bool true
               :user.property/prop-num 5
               :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")
           "Existing page has correct properties")
 
 
       (is (= {:user.property/rating 5.5}
       (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"
     (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")
           "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 (= {}
         (is (= {}
                (and b (readable-properties @conn b)))
                (and b (readable-properties @conn b)))
             ":template properties are ignored to not invalidate its property types"))
             ":template properties are ignored to not invalidate its property types"))
 
 
       (is (= {:logseq.task/deadline "Nov 26th, 2022"}
       (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")
           "deadline block has correct journal as property value")
 
 
       (is (= {:logseq.task/deadline "Nov 25th, 2022"}
       (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")
           "scheduled block converted to correct deadline")
 
 
       (is (= 1 (count (d/q '[:find [(pull ?b [*]) ...]
       (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")
           "Only one journal page exists when deadline is on same day as journal")
 
 
       (is (= {:logseq.task/priority "High"}
       (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")
           "priority block has correct property")
 
 
       (is (= {:logseq.task/status "Doing" :logseq.task/priority "Medium" :block/tags [:logseq.class/Task]}
       (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")
           "status block has correct task properties and class")
 
 
       (is (= #{:logseq.task/status :block/tags}
       (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")
           "old task properties like 'todo' are ignored")
 
 
       (is (= {:logseq.property/order-list-type "number"}
       (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")
           "numered block has correct property")
 
 
       (is (= #{"gpt"}
       (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")
           "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"}
       (is (= {:logseq.property.linked-references/includes #{"Oct 9th, 2024"}
               :logseq.property.linked-references/excludes #{"ref2"}}
               :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]))
                           [:logseq.property.linked-references/excludes :logseq.property.linked-references/includes]))
           "linked ref filters set correctly"))
           "linked ref filters set correctly"))
 
 
@@ -346,36 +351,44 @@
              (readable-properties @conn (find-block-by-property-value @conn :logseq.property/query "(property :prop-string)")))
              (readable-properties @conn (find-block-by-property-value @conn :logseq.property/query "(property :prop-string)")))
           "simple query block has correct query properties")
           "simple query block has correct query properties")
       (is (= "For example, here's a query with title text:"
       (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")
           "Text around a simple query block is set as a query's title")
       (is (= {:logseq.property.view/type "List View"
       (is (= {:logseq.property.view/type "List View"
               :logseq.property/query "{:query (task todo doing)}"
               :logseq.property/query "{:query (task todo doing)}"
               :block/tags [:logseq.class/Query]
               :block/tags [:logseq.class/Query]
               :logseq.property.table/ordered-columns [:block/title]}
               :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")
           "Advanced query has correct query properties")
       (is (= "tasks with todo and doing"
       (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")
           "Advanced query has custom title migrated")
 
 
       ;; Cards
       ;; Cards
       (is (= {:block/tags [:logseq.class/Card]}
       (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"))
           "None of the card properties are imported since they are deprecated"))
 
 
     (testing "tags convert to classes"
     (testing "tags convert to classes"
       (is (= :user.class/Quotes___life
       (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")
           "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")
           "When a class is used and referenced on the same page, there should only be one instance of it")
 
 
       (is (= ["life"]
       (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 %))))
                   (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"
     (testing "namespaces"
       (let [expand-children (fn expand-children [ent parent]
       (let [expand-children (fn expand-children [ent parent]
@@ -386,25 +399,25 @@
         (is (= [{:parent "n1" :child "x"}
         (is (= [{:parent "n1" :child "x"}
                 {:parent "x" :child "z"}
                 {:parent "x" :child "z"}
                 {:parent "x" :child "y"}]
                 {: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")
             "First namespace tests duplicate parent page name")
         (is (= [{:parent "n2" :child "x"}
         (is (= [{:parent "n2" :child "x"}
                 {:parent "x" :child "z"}
                 {:parent "x" :child "z"}
                 {:parent "n2" :child "alias"}]
                 {: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")))
             "First namespace tests duplicate child page name and built-in page name")))
 
 
     (testing "journal timestamps"
     (testing "journal timestamps"
       (is (= (date-time-util/journal-day->ms 20240207)
       (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")
           "journal pages are created on their journal day")
       (is (= (date-time-util/journal-day->ms 20240207)
       (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"))
           "journal blocks are created on their page's journal day"))
 
 
     (testing "db attributes"
     (testing "db attributes"
       (is (= true
       (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"))
           "Collapsed blocks are imported"))
 
 
     (testing "property :type changes"
     (testing "property :type changes"
@@ -419,7 +432,7 @@
              (get-in (d/entity @conn :user.property/description) [:block/schema :type]))
              (get-in (d/entity @conn :user.property/description) [:block/schema :type]))
           ":default property to :node (or any non :default value) remains :default")
           ":default property to :node (or any non :default value) remains :default")
       (is (= "[[Jakob]]"
       (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")
           ":default to :node property saves :default property value default with full text")
 
 
       (testing "with changes to upstream/existing property value"
       (testing "with changes to upstream/existing property value"
@@ -427,19 +440,19 @@
                (get-in (d/entity @conn :user.property/duration) [:block/schema :type]))
                (get-in (d/entity @conn :user.property/duration) [:block/schema :type]))
             ":number property to :default value changes to :default")
             ":number property to :default value changes to :default")
         (is (= "20"
         (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")
             "existing :number property value correctly saved as :default")
 
 
         (is (= {:block/schema {:type :default} :db/cardinality :db.cardinality/many}
         (is (= {:block/schema {:type :default} :db/cardinality :db.cardinality/many}
                (select-keys (d/entity @conn :user.property/people) [:block/schema :db/cardinality]))
                (select-keys (d/entity @conn :user.property/people) [:block/schema :db/cardinality]))
             ":node property to :default value changes to :default and keeps existing cardinality")
             ":node property to :default value changes to :default and keeps existing cardinality")
         (is (= #{"[[Jakob]] [[Gabriel]]"}
         (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")
             "existing :node property value correctly saved as :default with full text")
         (is (= #{"[[Gabriel]] [[Jakob]]"}
         (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")
             "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")
             "Previous :node property value still exists")
         (is (= 3 (count (find-block-by-property @conn :user.property/people)))
         (is (= 3 (count (find-block-by-property @conn :user.property/people)))
             "Converted property has correct number of property values")))
             "Converted property has correct number of property values")))
@@ -448,17 +461,20 @@
       (is (= #{:logseq.property/description :user.property/description}
       (is (= #{:logseq.property/description :user.property/description}
              (set (d/q '[:find [?ident ...] :where [?b :db/ident ?ident] [?b :block/name "description"]] @conn)))
              (set (d/q '[:find [?ident ...] :where [?b :db/ident ?ident] [?b :block/name "description"]] @conn)))
           "user description property is separate from built-in one")
           "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"))
           "user page is separate from built-in class"))
 
 
     (testing "multiline blocks"
     (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"
     (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?
         (is (set/subset?
              #{"type" "LargeLanguageModel"}
              #{"type" "LargeLanguageModel"}
              (->> page :block/refs (map #(:block/title (d/entity @conn (:db/id %)))) set))
              (->> 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 :block/path-refs (map #(:block/title (d/entity @conn (:db/id %)))) set))
             "Page has correct property and property value :block/path-refs"))
             "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?
         (is (set/subset?
              #{:logseq.task/status :logseq.class/Task}
              #{:logseq.task/status :logseq.class/Task}
              (->> block
              (->> block
@@ -485,7 +501,7 @@
             "Block has correct task tag and property :block/path-refs")))
             "Block has correct task tag and property :block/path-refs")))
 
 
     (testing "whiteboards"
     (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}
         (is (= {:user.property/prop-num 10}
                (readable-properties @conn block-with-props)))
                (readable-properties @conn block-with-props)))
         (is (= "block with props" (:block/title 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 (count @(:ignored-properties import-state))) "No ignored properties")
     (is (= 0 (->> @conn
     (is (= 0 (->> @conn
                   (d/q '[:find [?ident ...]
                   (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))
                   count))
         "Correct number of user classes")
         "Correct number of user classes")
 
 
@@ -511,7 +527,7 @@
 
 
     (testing "replacing refs in :block/title when :remove-inline-tags? set"
     (testing "replacing refs in :block/title when :remove-inline-tags? set"
       (is (= 2
       (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
                   :block/title
                   (re-seq db-content/id-ref-pattern)
                   (re-seq db-content/id-ref-pattern)
                   distinct
                   distinct
@@ -519,7 +535,7 @@
           "A block with ref names that start with same string has 2 distinct refs")
           "A block with ref names that start with same string has 2 distinct refs")
 
 
       (is (= 1
       (is (= 1
-             (->> (find-block-by-content @conn #"replace case insensitive")
+             (->> (db-test/find-block-by-content @conn #"replace case insensitive")
                   :block/title
                   :block/title
                   (re-seq db-content/id-ref-pattern)
                   (re-seq db-content/id-ref-pattern)
                   distinct
                   distinct
@@ -527,9 +543,9 @@
           "A block with different case of same ref names has 1 distinct ref"))
           "A block with different case of same ref names has 1 distinct ref"))
 
 
     (testing "tags convert to page, refs and page-tags"
     (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 [[")
         (is (string/starts-with? (str (:block/title block)) "Inception [[")
             "tagged block tag converts tag to page ref")
             "tagged block tag converts tag to page ref")
         (is (= [(:db/id tag-page)] (map :db/id (:block/refs block)))
         (is (= [(:db/id tag-page)] (map :db/id (:block/refs block)))
@@ -537,11 +553,11 @@
         (is (and tag-page (not (ldb/class? tag-page)))
         (is (and tag-page (not (ldb/class? tag-page)))
             "tag page is not a class")
             "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")
             "tagged page has existing page imported as a tag to page-tags")
         (is (= #{"LargeLanguageModel" "fun" "ai"}
         (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")))))
             "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
 (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"])
           files (mapv #(node-path/join file-graph-dir %) ["journals/2024_02_07.md" "pages/Interstellar.md"])
           conn (db-test/create-conn)
           conn (db-test/create-conn)
           _ (import-files-to-db files conn {:tag-classes ["movie"]})]
           _ (import-files-to-db files conn {:tag-classes ["movie"]})]
-
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
         "Created graph has no validation errors")
         "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")
       (is (= (:block/title block) "Inception")
           "tagged block with configured tag strips tag from content")
           "tagged block with configured tag strips tag from content")
       (is (= [:user.class/Movie]
       (is (= [:user.class/Movie]
              (:block/tags (readable-properties @conn block)))
              (:block/tags (readable-properties @conn block)))
           "tagged block has configured tag imported as a class")
           "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")
           "configured tag page in :tag-classes is a class")
       (is (and another-tag-page (not (ldb/class? another-tag-page)))
       (is (and another-tag-page (not (ldb/class? another-tag-page)))
           "unconfigured tag page is not a class")
           "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"))))
           "tagged page has configured tag imported as a class"))))
 
 
 (deftest-async export-files-with-property-classes-option
 (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}
     (is (= #{:user.class/Property :user.class/Movie :user.class/Class :user.class/Tool}
            (->> @conn
            (->> @conn
                 (d/q '[:find [?ident ...]
                 (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))
                 set))
         "All classes are correctly defined by :type")
         "All classes are correctly defined by :type")
 
 
@@ -597,8 +612,8 @@
                 set))
                 set))
         "Properties are correctly inferred for a class")
         "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")
       (is (= (:block/title block) "The Creator")
           "tagged block with configured tag strips tag from content")
           "tagged block with configured tag strips tag from content")
       (is (= [:user.class/Movie]
       (is (= [:user.class/Movie]
@@ -608,14 +623,14 @@
           "tagged block can have another property that references the same class it is tagged with,
           "tagged block can have another property that references the same class it is tagged with,
            without creating a duplicate class")
            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")
           "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")
           "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
 (deftest-async export-files-with-remove-inline-tags
   (p/let [file-graph-dir "test/resources/exporter-test-graph"
   (p/let [file-graph-dir "test/resources/exporter-test-graph"
@@ -625,7 +640,7 @@
 
 
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
     (is (empty? (map :entity (:errors (db-validate/validate-db! @conn))))
         "Created graph has no validation errors")
         "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")
                              "Inception #Movie")
         "block with tag preserves inline tag")))
         "block with tag preserves inline tag")))
 
 
@@ -654,7 +669,7 @@
              :user.class/Class :user.class/Tool :user.class/Whiteboard___Tool}
              :user.class/Class :user.class/Tool :user.class/Whiteboard___Tool}
            (->> @conn
            (->> @conn
                 (d/q '[:find [?ident ...]
                 (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))
                 set))
         "All classes are correctly defined by :type")
         "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]]
   (:require [cljs.test :refer [deftest is are]]
             [logseq.graph-parser.extract :as extract]
             [logseq.graph-parser.extract :as extract]
             [datascript.core :as d]
             [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
 ;; This is a copy of frontend.util.fs/multiplatform-reserved-chars for reserved chars testing
 (def multiplatform-reserved-chars ":\\*\\?\"<>|\\#\\\\")
 (def multiplatform-reserved-chars ":\\*\\?\"<>|\\#\\\\")
@@ -145,6 +144,6 @@
         page (first pages)]
         page (first pages)]
     (is (= (get-in page [:block/file :file/path]) "/whiteboards/foo.edn"))
     (is (= (get-in page [:block/file :file/path]) "/whiteboards/foo.edn"))
     (is (= (:block/name page) "foo"))
     (is (= (:block/name page) "foo"))
-    (is (ldb/whiteboard? page))
+    (is (= (:block/type page) "whiteboard"))
     (is (= (:block/title page) "Foo"))
     (is (= (:block/title page) "Foo"))
     (is (every? #(= (:block/parent %) [:block/uuid #uuid "a846e3b4-c41d-4251-80e1-be6978c36d8c"]) blocks))))
     (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)]
                                                                       (let [refs (:block/_refs page)]
                                                                         (and (or (zero? (count refs))
                                                                         (and (or (zero? (count refs))
                                                                                  (= #{db-id} (set (map :db/id 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)
       (when (seq orphaned-pages)
         (let [tx (mapv (fn [page] [:db/retractEntity (:db/id page)]) orphaned-pages)]
         (let [tx (mapv (fn [page] [:db/retractEntity (:db/id page)]) orphaned-pages)]
           (swap! txs-state (fn [state] (vec (concat state tx)))))))))
           (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)
            multiple-values-empty? (and (sequential? old-value)
                                        (contains? (set (map :db/ident old-value)) :logseq.property/empty-placeholder))
                                        (contains? (set (map :db/ident old-value)) :logseq.property/empty-placeholder))
            block' (assoc (outliner-core/block-with-updated-at {:db/id (:db/id block)})
            block' (assoc (outliner-core/block-with-updated-at {:db/id (:db/id block)})
-                        property-id value)
+                         property-id value)
            block-tx-data (cond-> block'
            block-tx-data (cond-> block'
                            (and status? (not (ldb/class-instance? (d/entity @conn :logseq.class/Task) block)))
                            (and status? (not (ldb/class-instance? (d/entity @conn :logseq.class/Task) block)))
                            (assoc :block/tags :logseq.class/Task))]
                            (assoc :block/tags :logseq.class/Task))]
@@ -160,6 +160,8 @@
             db-ident' (db-ident/ensure-unique-db-ident @conn db-ident)]
             db-ident' (db-ident/ensure-unique-db-ident @conn db-ident)]
         (assert (some? k-name)
         (assert (some? k-name)
                 (prn "property-id: " property-id ", property-name: " property-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
         (ldb/transact! conn
                        [(sqlite-util/build-new-property db-ident' schema {:title k-name})]
                        [(sqlite-util/build-new-property db-ident' schema {:title k-name})]
                        {:outliner-op :new-property})
                        {:outliner-op :new-property})
@@ -281,10 +283,16 @@
         _ (assert (qualified-keyword? property-id) "property-id should be a keyword")
         _ (assert (qualified-keyword? property-id) "property-id should be a keyword")
         block (d/entity @conn block-eid)
         block (d/entity @conn block-eid)
         db-attribute? (some? (db-schema/schema-for-db-based-graph property-id))]
         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
       (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}]
         (ldb/transact! conn [{:db/id (:db/id block) property-id v}]
                        {:outliner-op :save-block}))
                        {:outliner-op :save-block}))
+      :else
       (let [property (d/entity @conn property-id)
       (let [property (d/entity @conn property-id)
             _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
             _ (assert (some? property) (str "Property " property-id " doesn't exist yet"))
             property-type (get-in property [:block/schema :type] :default)
             property-type (get-in property [:block/schema :type] :default)
@@ -302,6 +310,8 @@
   (assert property-id "property-id is nil")
   (assert property-id "property-id is nil")
   (throw-error-if-read-only-property property-id)
   (throw-error-if-read-only-property property-id)
   (let [block-eids (map ->eid block-ids)
   (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)
         property (d/entity @conn property-id)
         _ (when (= (:db/ident property) :logseq.property/parent)
         _ (when (= (:db/ident property) :logseq.property/parent)
             (outliner-validate/validate-parent-property
             (outliner-validate/validate-parent-property
@@ -553,7 +563,6 @@
           (when (seq values)
           (when (seq values)
             (let [value-property-tx (map (fn [id]
             (let [value-property-tx (map (fn [id]
                                            {:db/id id
                                            {:db/id id
-                                            :block/type "closed value"
                                             :block/closed-value-property (:db/id property)})
                                             :block/closed-value-property (:db/id property)})
                                          (map :db/id values))
                                          (map :db/id values))
                   property-tx (outliner-core/block-with-updated-at {:db/id (:db/id property)})]
                   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
 (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]
   (:require [clojure.string :as string]
             [datascript.core :as d]
             [datascript.core :as d]
             [logseq.db :as ldb]
             [logseq.db :as ldb]
             [logseq.common.date :as common-date]
             [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
 (defn ^:api validate-page-title-characters
   "Validates characters that must not be in a page title"
   "Validates characters that must not be in a page title"
@@ -42,30 +44,6 @@
                      :payload {:message "Built-in pages can't be edited"
                      :payload {:message "Built-in pages can't be edited"
                                :type :warning}}))))
                                :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]
 (defn- validate-unique-by-parent-and-name [db entity new-title]
   (when-let [_res (seq (d/q '[:find [?b ...]
   (when-let [_res (seq (d/q '[:find [?b ...]
                               :in $ ?eid ?type ?title
                               :in $ ?eid ?type ?title
@@ -88,50 +66,49 @@
 (defn- validate-unique-for-page
 (defn- validate-unique-for-page
   [db new-title {:block/keys [tags] :as entity}]
   [db new-title {:block/keys [tags] :as entity}]
   (cond
   (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)
     (: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
 (defn ^:api validate-unique-by-name-tag-and-block-type
   "Validates uniqueness of nodes for the following cases:
   "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]
   [db new-title entity]
   (when (ldb/page? entity)
   (when (ldb/page? entity)
     (validate-unique-for-page db new-title 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-unique-by-name-tag-and-block-type db new-title existing-block-entity)
   (validate-disallow-page-with-journal-name 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
   "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 to have a relationship with their own type. May consider allowing more
   page types if they don't cause systemic bugs"
   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"
                      :payload {:message "Can't set this page as a parent because the child page is a different type"
                                :type :warning}
                                :type :warning}
                      :blocks (map #(select-keys % [:db/id :block/title]) (remove ldb/class? child-ents))}))))
                      :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]
             [logseq.outliner.pipeline :as outliner-pipeline]
             [clojure.string :as string]
             [clojure.string :as string]
             [logseq.db.test.helper :as db-test]
             [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]
 (defn- get-blocks [db]
   (->> (d/q '[:find (pull ?b [* {:block/path-refs [:block/name :db/id]}])
   (->> (d/q '[:find (pull ?b [* {:block/path-refs [:block/name :db/id]}])
               :in $
               :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)
             db)
        (map first)))
        (map first)))
 
 
@@ -48,13 +50,14 @@
           updated-blocks (->> (get-blocks @conn)
           updated-blocks (->> (get-blocks @conn)
                               ;; Only keep enough of content to uniquely identify block
                               ;; Only keep enough of content to uniquely identify block
                               (map #(hash-map :block/title (re-find #"\w+" (:block/title %))
                               (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"
       (is (= [{:block/title "parent"
-               :path-ref-names #{"page1" "bar"}}
+               :path-ref-names (set/union page-tag-refs #{"page1" "bar"})}
               {:block/title "child"
               {:block/title "child"
-               :path-ref-names #{"page1" "bar" "baz"}}
+               :path-ref-names (set/union page-tag-refs #{"page1" "bar" "baz"})}
               {:block/title "grandchild"
               {:block/title "grandchild"
-               :path-ref-names #{"page1" "bar" "baz" "bing"}}]
+               :path-ref-names (set/union page-tag-refs #{"page1" "bar" "baz" "bing"})}]
              updated-blocks)))))
              updated-blocks)))))
 
 
 (deftest block-content-refs
 (deftest block-content-refs

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

@@ -3,7 +3,8 @@
             [datascript.core :as d]
             [datascript.core :as d]
             [logseq.outliner.property :as outliner-property]
             [logseq.outliner.property :as outliner-property]
             [logseq.db.frontend.property :as db-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!
 (deftest upsert-property!
   (testing "Creates a property"
   (testing "Creates a property"
@@ -228,7 +229,7 @@
     (testing "Add choice successfully"
     (testing "Add choice successfully"
       (let [_ (outliner-property/upsert-closed-value! conn :user.property/num {:value 3})
       (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))]
             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]
         (is (= [2 3]
                (map db-property/closed-value-content (:block/_closed-value-property (d/entity @conn :user.property/num)))))))
                (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.outliner.validate :as outliner-validate]
             [logseq.db.test.helper :as db-test]))
             [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
 (deftest validate-block-title-unique-for-properties
   (let [conn (db-test/create-conn-with-blocks
   (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?
     (is (nil?
          (outliner-validate/validate-unique-by-name-tag-and-block-type
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
           @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")
         "Allow user property to have same name as built-in property")
 
 
     (is (thrown-with-msg?
     (is (thrown-with-msg?
          js/Error
          js/Error
-         #"Duplicate property"
+         #"Duplicate page"
          (outliner-validate/validate-unique-by-name-tag-and-block-type
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
           @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")))
         "Disallow duplicate user property")))
 
 
 (deftest validate-block-title-unique-for-pages
 (deftest validate-block-title-unique-for-pages
   (let [conn (db-test/create-conn-with-blocks
   (let [conn (db-test/create-conn-with-blocks
               [{:page {:block/title "page1"}}
               [{:page {:block/title "page1"}}
+               {:page {:block/title "another page"}}
                {:page {:block/title "Apple" :build/tags [:Company]}}
                {:page {:block/title "Apple" :build/tags [:Company]}}
+               {:page {:block/title "Another Company" :build/tags [:Company]}}
                {:page {:block/title "Banana" :build/tags [:Fruit]}}])]
                {:page {:block/title "Banana" :build/tags [:Fruit]}}])]
 
 
     (is (thrown-with-msg?
     (is (thrown-with-msg?
          js/Error
          js/Error
-         #"Duplicate page by tag"
+         #"Duplicate page"
          (outliner-validate/validate-unique-by-name-tag-and-block-type
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
           @conn
           "Apple"
           "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")
         "Disallow duplicate page with tag")
     (is (nil?
     (is (nil?
          (outliner-validate/validate-unique-by-name-tag-and-block-type
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
           @conn
           "Apple"
           "Apple"
-          (find-block-by-content conn "Banana")))
+          (db-test/find-page-by-title @conn "Banana")))
         "Allow page with same name for different tag")
         "Allow page with same name for different tag")
 
 
     (is (thrown-with-msg?
     (is (thrown-with-msg?
          js/Error
          js/Error
-         #"Duplicate page without tag"
+         #"Duplicate page"
          (outliner-validate/validate-unique-by-name-tag-and-block-type
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
           @conn
           "page1"
           "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")
         "Disallow duplicate page without tag")
 
 
     (is (nil?
     (is (nil?
          (outliner-validate/validate-unique-by-name-tag-and-block-type
          (outliner-validate/validate-unique-by-name-tag-and-block-type
           @conn
           @conn
           "Apple"
           "Apple"
-          (find-block-by-content conn "Fruit")))
+          (db-test/find-page-by-title @conn "Fruit")))
         "Allow class to have same name as a page")))
         "Allow class to have same name as a page")))
 
 
 (deftest validate-parent-property
 (deftest validate-parent-property
@@ -77,11 +71,11 @@
                :pages-and-blocks
                :pages-and-blocks
                [{:page {:block/title "page1"}}
                [{:page {:block/title "page1"}}
                 {:page {:block/title "page2"}}]})
                 {: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"
     (testing "valid parent and child combinations"
       (is (nil? (outliner-validate/validate-parent-property page1 [page2]))
       (is (nil? (outliner-validate/validate-parent-property page1 [page2]))
@@ -99,7 +93,52 @@
         class1 page1
         class1 page1
         page1 class1
         page1 class1
         property page1
         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
 ;; Try as many of the validations against a new graph to confirm
 ;; that validations make sense and are valid for a new graph
 ;; that validations make sense and are valid for a new graph
@@ -107,7 +146,11 @@
   (let [conn (db-test/create-conn)]
   (let [conn (db-test/create-conn)]
 
 
     (testing "Validate pages"
     (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 {})]
             page-errors (atom {})]
         (doseq [page pages]
         (doseq [page pages]
           (try
           (try
@@ -123,9 +166,10 @@
             "Default pages shouldn't have any validation errors")))
             "Default pages shouldn't have any validation errors")))
 
 
     (testing "Validate property relationships"
     (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)]
                                       :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"})]
   (let [icon #(ui/tabler-icon (name %1) {:class "scale-90 pr-1 opacity-80"})]
     (ui/dropdown-menu-content
     (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
         ;; 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
       ;; group
-      (ui/dropdown-menu-group
+     (ui/dropdown-menu-group
         ;; items
         ;; items
-        (ui/dropdown-menu-item (icon :users) "Team")
+      (ui/dropdown-menu-item (icon :users) "Team")
         ;; sub menu
         ;; 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
         ;; 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
 (rum/defc sample-context-menu-content
   []
   []
   (let [icon #(ui/tabler-icon (name %1) {:class "scale-90 pr-1 opacity-80"})]
   (let [icon #(ui/tabler-icon (name %1) {:class "scale-90 pr-1 opacity-80"})]
     (ui/context-menu
     (ui/context-menu
       ;; trigger
       ;; 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
       ;; 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
         ;; 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
         ;; 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
 (rum/defc sample-form-basic
   []
   []
   [:div.border.p-6.rounded.bg-gray-01
   [:div.border.p-6.rounded.bg-gray-01
    (let [form-ctx (form-core/use-form
    (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)
          handle-submit (:handleSubmit form-ctx)
          on-submit-valid (handle-submit
          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
      (ui/form-provider form-ctx
-       [:form
-        {:on-submit on-submit-valid}
+                       [:form
+                        {:on-submit on-submit-valid}
 
 
         ;; field item
         ;; 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
         ;; radio
-        (ui/form-field {:name "notification"}
+                        (ui/form-field {:name "notification"}
           ;; item render
           ;; 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
         ;; 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
         ;; 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
 (rum/defc sample-date-picker
   []
   []
   (let [[open? set-open!] (rum/use-state false)
   (let [[open? set-open!] (rum/use-state false)
         [date set-date!] (rum/use-state (js/Date.))]
         [date set-date!] (rum/use-state (js/Date.))]
     (ui/popover
     (ui/popover
-      {:open open?
-       :on-open-change (fn [o] (set-open! o))}
+     {:open open?
+      :on-open-change (fn [o] (set-open! o))}
       ;; trigger
       ;; 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
       ;; 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
 (rum/defc sample-dialog-basic
   []
   []
   (let [[open? set-open!] (rum/use-state false)]
   (let [[open? set-open!] (rum/use-state false)]
     (ui/dialog
     (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 []
 (rum/defc page []
   (ui/tooltip-provider
   (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
      ;; 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
       ;; 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
       ;; 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
      ;; 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
       ;; 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
       ;; 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
      ;; 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
      ;; 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
      ;; 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
            ;; trigger
-           (ui/select-trigger
-             (ui/select-value {:placeholder "Select a fruit"}))
+                     (ui/select-trigger
+                      (ui/select-value {:placeholder "Select a fruit"}))
            ;; content
            ;; 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
      ;; 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
      ;; 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
 (defn- get-head-container
   []
   []
@@ -499,44 +512,44 @@
 
 
   (let [el-ref (rum/use-ref nil)]
   (let [el-ref (rum/use-ref nil)]
     (rum/use-effect!
     (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
      [:div.charlie-table-header
      [: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-sub-trigger (util/lsui-wrap "ContextMenuSubTrigger"))
 (def context-menu-radio-group (util/lsui-wrap "ContextMenuRadioGroup"))
 (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 dialog-core/dialog)
 (def dialog-portal dialog-core/dialog-portal)
 (def dialog-portal dialog-core/dialog-portal)
 (def dialog-overlay dialog-core/dialog-overlay)
 (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-slider": "^1.1.2",
     "@radix-ui/react-slot": "^1.1.0",
     "@radix-ui/react-slot": "^1.1.0",
     "@radix-ui/react-switch": "^1.0.3",
     "@radix-ui/react-switch": "^1.0.3",
+    "@radix-ui/react-tabs": "^1.1.1",
     "@radix-ui/react-toast": "^1.1.5",
     "@radix-ui/react-toast": "^1.1.5",
     "@radix-ui/react-toggle": "^1.0.3",
     "@radix-ui/react-toggle": "^1.0.3",
     "@radix-ui/react-toggle-group": "^1.0.4",
     "@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 { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group'
 import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
 import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'
 import * as uniqolor from 'uniqolor'
 import * as uniqolor from 'uniqolor'
+import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'
 
 
 declare global {
 declare global {
   var LSUI: any
   var LSUI: any
@@ -185,6 +186,7 @@ const shadui = {
   TooltipContent, TooltipProvider, TooltipPortal,
   TooltipContent, TooltipProvider, TooltipPortal,
   Toggle, ToggleGroup, ToggleGroupItem,
   Toggle, ToggleGroup, ToggleGroupItem,
   Avatar, AvatarImage, AvatarFallback,
   Avatar, AvatarImage, AvatarFallback,
+  Tabs, TabsContent, TabsList, TabsTrigger
 }
 }
 
 
 function setupGlobals() {
 function setupGlobals() {
@@ -203,4 +205,4 @@ setupGlobals()
 
 
 export {
 export {
   setupGlobals
   setupGlobals
-}
+}

+ 72 - 6
packages/ui/yarn.lock

@@ -2267,6 +2267,16 @@
     "@radix-ui/react-primitive" "1.0.3"
     "@radix-ui/react-primitive" "1.0.3"
     "@radix-ui/react-slot" "1.0.2"
     "@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]":
 "@radix-ui/[email protected]":
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.0.tgz#37595b1f16ec7f228d698590e78eeed18ff218ae"
@@ -2392,6 +2402,11 @@
   dependencies:
   dependencies:
     "@babel/runtime" "^7.13.10"
     "@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]":
 "@radix-ui/[email protected]":
   version "1.0.0"
   version "1.0.0"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.0.0.tgz#35b7826fa262fd84370faef310e627161dffa76b"
   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-callback-ref" "1.0.1"
     "@radix-ui/react-use-controllable-state" "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":
 "@radix-ui/react-select@^1.2.2":
   version "1.2.2"
   version "1.2.2"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-select/-/react-select-1.2.2.tgz#caa981fa0d672cf3c1b2a5240135524e69b32181"
   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-previous" "1.0.1"
     "@radix-ui/react-use-size" "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":
 "@radix-ui/react-toast@^1.1.5":
   version "1.1.5"
   version "1.1.5"
   resolved "https://registry.yarnpkg.com/@radix-ui/react-toast/-/react-toast-1.1.5.tgz#f5788761c0142a5ae9eb97f0051fd3c48106d9e6"
   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"
   resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.1.tgz#d7088281559ab2778424279b0877da3c392d5a3d"
   integrity sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==
   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"
   version "4.2.3"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
   integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
@@ -8975,6 +9018,15 @@ [email protected]:
     is-fullwidth-code-point "^3.0.0"
     is-fullwidth-code-point "^3.0.0"
     strip-ansi "^6.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:
 string_decoder@^1.1.1:
   version "1.3.0"
   version "1.3.0"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
   resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e"
@@ -8989,8 +9041,14 @@ string_decoder@~1.1.1:
   dependencies:
   dependencies:
     safe-buffer "~5.1.0"
     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"
   version "6.0.1"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
   integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
@@ -9650,8 +9708,16 @@ wordwrap@^1.0.0:
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
   resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb"
   integrity sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==
   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"
   version "7.0.0"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43"
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==
   integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==

+ 3 - 3
resources/package.json

@@ -43,8 +43,7 @@
     "posthog-js": "1.10.2",
     "posthog-js": "1.10.2",
     "semver": "7.5.2",
     "semver": "7.5.2",
     "socks-proxy-agent": "8.0.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": {
   "devDependencies": {
     "@electron-forge/cli": "^7.3.1",
     "@electron-forge/cli": "^7.3.1",
@@ -57,7 +56,8 @@
     "@electron/rebuild": "3.2.10",
     "@electron/rebuild": "3.2.10",
     "electron": "31.7.5",
     "electron": "31.7.5",
     "electron-builder": "25.1.8",
     "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": {
   "resolutions": {
     "**/electron": "31.7.5",
     "**/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]}
                                              {:logseq.property.class/properties [:block/title]}
                                              {:property/schema.classes [:block/title]}
                                              {:property/schema.classes [:block/title]}
                                              {:logseq.property/parent [:block/title]}
                                              {:logseq.property/parent [:block/title]}
+                                             {:block/tags [:block/title]}
                                              {:block/refs [:block/title]}]) ...]
                                              {:block/refs [:block/title]}]) ...]
                             :in $
                             :in $
                             :where [?b :db/ident ?ident]]
                             :where [?b :db/ident ?ident]]
@@ -376,11 +377,12 @@
                             (map (fn [m]
                             (map (fn [m]
                                    (let [props (->> (db-property/properties m)
                                    (let [props (->> (db-property/properties m)
                                                     (into {}))]
                                                     (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
                                                              :logseq.property.class/properties :logseq.property/parent
                                                              :db/cardinality :property/schema.classes :block/refs])
                                                              :db/cardinality :property/schema.classes :block/refs])
                                        (seq props)
                                        (seq props)
                                        (assoc :block/properties (-> (update-keys props name)
                                        (assoc :block/properties (-> (update-keys props name)
+                                                                    (dissoc "tags")
                                                                     (update-vals (fn [v]
                                                                     (update-vals (fn [v]
                                                                                    (if (:db/id v)
                                                                                    (if (:db/id v)
                                                                                      (db-property/property-value-content (d/entity db (:db/id v)))
                                                                                      (db-property/property-value-content (d/entity db (:db/id v)))
@@ -391,6 +393,8 @@
                                        (update :logseq.property/parent :block/title)
                                        (update :logseq.property/parent :block/title)
                                        (seq (:property/schema.classes m))
                                        (seq (:property/schema.classes m))
                                        (update :property/schema.classes #(set (map :block/title %)))
                                        (update :property/schema.classes #(set (map :block/title %)))
+                                       (seq (:block/tags m))
+                                       (update :block/tags #(set (map :block/title %)))
                                        (seq (:block/refs m))
                                        (seq (:block/refs m))
                                        (update :block/refs #(set (map :block/title %)))))))
                                        (update :block/refs #(set (map :block/title %)))))))
                             set)))))
                             set)))))

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

@@ -91,7 +91,8 @@
   (let [file-concepts (->>
   (let [file-concepts (->>
                        ;; from logseq.db.frontend.schema
                        ;; from logseq.db.frontend.schema
                        [:block/namespace :block/properties-text-values :block/pre-block :recent/pages :block/file :block/properties-order
                        [: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)
                        (map str)
                        (into [;; e.g. block/properties :title
                        (into [;; e.g. block/properties :title
                               "block/properties :"
                               "block/properties :"

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

@@ -21,13 +21,14 @@
          :cell (fn [_table row _column]
          :cell (fn [_table row _column]
                  (component-block/page-cp {} row))
                  (component-block/page-cp {} row))
          :type :string}
          :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
         {:id :block.temp/refs-count
          :name (t :page/backlinks)
          :name (t :page/backlinks)
          :cell (fn [_table row _column] (:block.temp/refs-count row))
          :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 :as query]
             [frontend.components.query.builder :as query-builder-component]
             [frontend.components.query.builder :as query-builder-component]
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
-            [frontend.components.title :as title]
             [frontend.components.select :as select]
             [frontend.components.select :as select]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
@@ -701,7 +700,9 @@
      (when (and show-icon? (not tag?))
      (when (and show-icon? (not tag?))
        (let [own-icon (get page-entity (pu/get-pid :logseq.property/icon))
        (let [own-icon (get page-entity (pu/get-pid :logseq.property/icon))
              emoji? (and (map? own-icon) (= (:type own-icon) :emoji))]
              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"))}
            [:span {:class (str "icon-emoji-wrap " (when emoji? "as-emoji"))}
             icon])))
             icon])))
      [:span
      [:span
@@ -722,7 +723,7 @@
                                (->elem :span (map-inline config label))
                                (->elem :span (map-inline config label))
 
 
                                show-unique-title?
                                show-unique-title?
-                               (title/block-unique-title page-entity)
+                               (block-handler/block-unique-title page-entity)
 
 
                                :else
                                :else
                                (let [title (:block/title page-entity)
                                (let [title (:block/title page-entity)
@@ -755,7 +756,7 @@
                                          (db-content/content-id-ref->page s (:block/refs page-entity))
                                          (db-content/content-id-ref->page s (:block/refs page-entity))
                                          :else
                                          :else
                                          s)
                                          s)
-                                     s (if tag? (str "#" s) s)]
+                                     s (if (and tag? (not (:hide-tag-symbol? config))) (str "#" s) s)]
                                  (if (ldb/page? page-entity)
                                  (if (ldb/page? page-entity)
                                    s
                                    s
                                    (block-title config page-entity))))]
                                    (block-title config page-entity))))]
@@ -2588,8 +2589,9 @@
 (rum/defcs block-tag <
 (rum/defcs block-tag <
   (rum/local false ::hover?)
   (rum/local false ::hover?)
   [state block tag config popup-opts]
   [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))
      {:key (str "tag-" (:db/id tag))
       :on-mouse-over #(reset! *hover? true)
       :on-mouse-over #(reset! *hover? true)
       :on-mouse-out #(reset! *hover? false)
       :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))}
                                :on-click #(db-property-handler/delete-property-value! (:db/id block) :block/tags (:db/id tag))}
                               "Remove tag")])
                               "Remove tag")])
                           popup-opts))}
                           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
      (page-cp (assoc config
+                     :disable-preview? true
                      :tag? 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
 (rum/defc tags-cp
   "Tags without inline or hidden tags"
   "Tags without inline or hidden tags"
@@ -2635,14 +2642,15 @@
                       (:block/tags block)
                       (:block/tags block)
                       (remove (fn [t]
                       (remove (fn [t]
                                 (or (ldb/inline-tag? (:block/raw-title block) 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
           popup-opts {:align :end
                       :content-props {:on-click (fn [] (shui/popup-hide!))
                       :content-props {:on-click (fn [] (shui/popup-hide!))
                                       :class "w-60"}}
                                       :class "w-60"}}
           tags-count (count block-tags)]
           tags-count (count block-tags)]
       (when (seq block-tags)
       (when (seq block-tags)
         (if (< tags-count 3)
         (if (< tags-count 3)
-          [:div.block-tags
+          [:div.block-tags.gap-1
            (for [tag block-tags]
            (for [tag block-tags]
              (rum/with-key
              (rum/with-key
                (block-tag block tag config popup-opts)
                (block-tag block tag config popup-opts)
@@ -2653,13 +2661,14 @@
                                                  (fn []
                                                  (fn []
                                                    (for [tag block-tags]
                                                    (for [tag block-tags]
                                                      [:div.flex.flex-row.items-center.gap-1
                                                      [: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
                                                       (page-cp (assoc config
                                                                       :tag? true
                                                                       :tag? true
                                                                       :disable-preview? true
                                                                       :disable-preview? true
@@ -2672,7 +2681,7 @@
                               :tag? true
                               :tag? true
                               :disable-preview? true
                               :disable-preview? true
                               :disable-click? true) tag)])
                               :disable-click? true) tag)])
-           [:div.text-sm.opacity-50
+           [:div.text-sm.opacity-50.ml-1
             (str "+" (- tags-count 2))]])))))
             (str "+" (- tags-count 2))]])))))
 
 
 (rum/defc block-positioned-properties
 (rum/defc block-positioned-properties

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

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

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

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

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

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

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

@@ -446,7 +446,7 @@
   flex: 1;
   flex: 1;
 
 
   .page {
   .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 :as db]
             [frontend.db-mixins :as db-mixins]
             [frontend.db-mixins :as db-mixins]
             [logseq.shui.ui :as shui]
             [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
 (rum/defc configure-property < rum/reactive db-mixins/query
   [page]
   [page]
   (let [page (db/sub-block (:db/id 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.file-based.datetime :as datetime-comp]
             [frontend.components.search :as search]
             [frontend.components.search :as search]
             [frontend.components.svg :as svg]
             [frontend.components.svg :as svg]
-            [frontend.components.title :as title]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.context.i18n :refer [t]]
             [frontend.context.i18n :refer [t]]
             [frontend.date :as date]
             [frontend.date :as date]
@@ -19,6 +18,7 @@
             [frontend.handler.paste :as paste-handler]
             [frontend.handler.paste :as paste-handler]
             [frontend.handler.property.util :as pu]
             [frontend.handler.property.util :as pu]
             [frontend.handler.search :as search-handler]
             [frontend.handler.search :as search-handler]
+            [frontend.handler.block :as block-handler]
             [frontend.mixins :as mixins]
             [frontend.mixins :as mixins]
             [frontend.search :refer [fuzzy-search]]
             [frontend.search :refer [fuzzy-search]]
             [frontend.state :as state]
             [frontend.state :as state]
@@ -35,7 +35,8 @@
             [logseq.shui.ui :as shui]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [promesa.core :as p]
             [react-draggable]
             [react-draggable]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [logseq.db.frontend.class :as db-class]))
 
 
 (defn filter-commands
 (defn filter-commands
   [page? commands]
   [page? commands]
@@ -156,7 +157,12 @@
                            ;; reorder, shortest and starts-with first.
                            ;; reorder, shortest and starts-with first.
                            (let [matched-pages-with-new-page
                            (let [matched-pages-with-new-page
                                  (fn [partial-matched-pages]
                                  (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)))))
                                            (and db-tag? (some ldb/class? (:block/_alias (db/get-page q)))))
                                      partial-matched-pages
                                      partial-matched-pages
                                      (if db-tag?
                                      (if db-tag?
@@ -215,7 +221,7 @@
                                             (if (ldb/class? target)
                                             (if (ldb/class? target)
                                               (str (:block/title block) " -> alias: " (:block/title target))
                                               (str (:block/title block) " -> alias: " (:block/title target))
                                               (:block/title block)))
                                               (:block/title block)))
-                                          (title/block-unique-title block))]
+                                          (block-handler/block-unique-title block))]
                               (search-handler/highlight-exact-query title q))]]))
                               (search-handler/highlight-exact-query title q))]]))
          :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (if db-tag?
          :empty-placeholder [:div.text-gray-500.text-sm.px-4.py-2 (if db-tag?
                                                                     "Search for a 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
      [:div.cp__file-sync-page-histories-right
       [:h1.title.text-xl
       [:h1.title.text-xl
        "Current version"]
        "Current version"]
-      (page/page-blocks-cp (state/get-current-repo) page-entity nil)]
+      (page/page-blocks-cp page-entity nil)]
 
 
      ;; ready loading
      ;; ready loading
      [:div.flex.items-center.h-full.justify-center.w-full.absolute.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
 (defn get-node-icon-cp
   [node-entity opts]
   [node-entity opts]
   (let [opts' (merge {:size 14} 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)))
     (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
       [:div.icon-cp-container.flex.items-center
        (merge {:style {:color (or (:color node-icon) "inherit")}}
        (merge {:style {:color (or (:color node-icon) "inherit")}}

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

@@ -154,53 +154,45 @@
 
 
     (if loading?
     (if loading?
       (ui/skeleton)
       (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
 (rum/defcs class-objects < rum/reactive db-mixins/query mixins/container-id
   [state class {:keys [current-page? sidebar?]}]
   [state class {:keys [current-page? sidebar?]}]
@@ -251,35 +243,32 @@
      [])
      [])
 
 
     (when (false? loading?)
     (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
                                ;; 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
 ;; Show all nodes containing the given property
 (rum/defcs property-related-objects < rum/reactive db-mixins/query mixins/container-id
 (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
 (rum/defcs page-blocks-cp < rum/reactive db-mixins/query
   {:will-mount (fn [state]
   {:will-mount (fn [state]
-                 (let [page-e (second (:rum/args state))
+                 (let [page-e (first (:rum/args state))
                        page-name (:block/name page-e)]
                        page-name (:block/name page-e)]
                    (when (and page-name
                    (when (and page-name
                               (db/journal-page? page-name)
                               (db/journal-page? page-name)
@@ -204,7 +204,7 @@
                                   (date/journal-title->int (date/today))))
                                   (date/journal-title->int (date/today))))
                      (state/pub-event! [:journal/insert-template page-name])))
                      (state/pub-event! [:journal/insert-template page-name])))
                  state)}
                  state)}
-  [state repo page-e {:keys [sidebar? whiteboard?] :as config}]
+  [state page-e {:keys [sidebar? whiteboard?] :as config}]
   (when page-e
   (when page-e
     (let [page-name (or (:block/name page-e)
     (let [page-name (or (:block/name page-e)
                         (str (:block/uuid page-e)))
                         (str (:block/uuid page-e)))
@@ -220,44 +220,35 @@
                      (remove (fn [b] (some? (get b (:db/ident block)))) children)
                      (remove (fn [b] (some? (get b (:db/ident block)))) children)
 
 
                      :else
                      :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
 (rum/defc today-queries < rum/reactive
   [repo today? sidebar?]
   [repo today? sidebar?]
@@ -301,10 +292,11 @@
 (rum/defc page-title-editor < rum/reactive
 (rum/defc page-title-editor < rum/reactive
   [page {:keys [*input-value *title-value *edit? untitled? page-name old-name whiteboard-page?]}]
   [page {:keys [*input-value *title-value *edit? untitled? page-name old-name whiteboard-page?]}]
   (let [input-ref (rum/create-ref)
   (let [input-ref (rum/create-ref)
+        tag-idents (map :db/ident (:block/tags page))
         collide? #(and (not= (util/page-name-sanity-lc page-name)
         collide? #(and (not= (util/page-name-sanity-lc page-name)
                              (util/page-name-sanity-lc @*title-value))
                              (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)]
         rollback-fn #(let [old-name (if untitled? "" old-name)]
                        (reset! *title-value old-name)
                        (reset! *title-value old-name)
                        (gobj/set (rum/deref input-ref) "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-slot :page-head-actions-slotted nil)
        (plugins/hook-ui-items :pagebar)])))
        (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
 ;; A page is just a logical block
 (rum/defcs ^:large-vars/cleanup-todo page-inner < rum/reactive db-mixins/query mixins/container-id
 (rum/defcs ^:large-vars/cleanup-todo page-inner < rum/reactive db-mixins/query mixins/container-id
   (rum/local false ::all-collapsed?)
   (rum/local false ::all-collapsed?)
@@ -605,9 +656,8 @@
 
 
            (if (and whiteboard-page? (not sidebar?))
            (if (and whiteboard-page? (not sidebar?))
              [:div ((state/get-component :whiteboard/tldraw-preview) (:block/uuid page))] ;; FIXME: this is not reactive
              [: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
                 [:div.flex.flex-row.space-between
                  (when (and (or (mobile-util/native-platform?) (util/mobile?)) (not db-based?))
                  (when (and (or (mobile-util/native-platform?) (util/mobile?)) (not db-based?))
                    [:div.flex.flex-row.pr-2
                    [:div.flex.flex-row.pr-2
@@ -625,14 +675,9 @@
                                           :preview? preview?})))
                                           :preview? preview?})))
                  (lsp-pagebar-slot)])
                  (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?))
               (when (and block? (not sidebar?) (not whiteboard?))
                 (let [config (merge config {:id "block-parent"
                 (let [config (merge config {:id "block-parent"
@@ -640,10 +685,13 @@
                   [:div.mb-4
                   [:div.mb-4
                    (component-block/breadcrumb config repo block-id {:level-limit 3})]))
                    (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
               [: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?)
            (when (not preview?)
              [:div {:style {:padding-left 9}}
              [:div {:style {:padding-left 9}}

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

@@ -65,8 +65,6 @@
   @apply rounded-sm;
   @apply rounded-sm;
 
 
   &.title {
   &.title {
-    @apply mb-3;
-
     .block-main-container {
     .block-main-container {
       @apply gap-2;
       @apply gap-2;
     }
     }
@@ -258,5 +256,5 @@ html.is-native-ios {
 }
 }
 
 
 .ls-page-blocks {
 .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.handler.property.util :as pu]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.db :as db]
+            [frontend.db.model :as db-model]
             [frontend.db-mixins :as db-mixins]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.async :as db-async]
             [frontend.db.async :as db-async]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.db-based.property :as db-property-handler]
@@ -99,7 +100,8 @@
                       add-class-property? (and (ldb/class? block) class-schema?)]
                       add-class-property? (and (ldb/class? block) class-schema?)]
                 (when *property (reset! *property property))
                 (when *property (reset! *property property))
                 (p/do!
                 (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))
                  (when (= (:type schema) :node) (reset! *show-class-select? true))
                  (components-pu/update-property! property property-name schema)
                  (components-pu/update-property! property property-name schema)
                  (cond
                  (cond
@@ -142,30 +144,42 @@
 (rum/defc property-select
 (rum/defc property-select
   [exclude-properties select-opts]
   [exclude-properties select-opts]
   (let [[properties set-properties!] (rum/use-state nil)
   (let [[properties set-properties!] (rum/use-state nil)
+        [classes set-classes!] (rum/use-state nil)
         [excluded-properties set-excluded-properties!] (rum/use-state nil)]
         [excluded-properties set-excluded-properties!] (rum/use-state nil)]
     (rum/use-effect!
     (rum/use-effect!
      (fn []
      (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-properties! (remove exclude-properties properties))
          (set-excluded-properties! (->> properties
          (set-excluded-properties! (->> properties
                                         (filter exclude-properties)
                                         (filter exclude-properties)
                                         (map :block/title)
                                         (map :block/title)
                                         set))))
                                         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
 (rum/defc property-icon
   [property property-type]
   [property property-type]
@@ -194,7 +208,7 @@
   (fn [{:keys [value label]}]
   (fn [{:keys [value label]}]
     (reset! *property-key (if (uuid? value) label value))
     (reset! *property-key (if (uuid? value) label value))
     (let [property (when (uuid? value) (db/entity [:block/uuid 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! *show-new-property-config? true))
       (reset! *property property)
       (reset! *property property)
       (when property
       (when property
@@ -221,10 +235,26 @@
                  (not (seq (:property/closed-values property))))
                  (not (seq (:property/closed-values property))))
             (pv/<create-new-block! block 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)
             (or (not= :default type)
                 (and (= :default type) (seq (:property/closed-values property))))
                 (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
 (rum/defc property-key-title
   [block property class-schema?]
   [block property class-schema?]
@@ -333,17 +363,20 @@
         *show-new-property-config? (::show-new-property-config? state)
         *show-new-property-config? (::show-new-property-config? state)
         *show-class-select? (::show-class-select? state)
         *show-class-select? (::show-class-select? state)
         *property-schema (::property-schema state)
         *property-schema (::property-schema state)
-        block-type (keyword (get block :block/type :block))
         page? (ldb/page? 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]
         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))
                                (or (contains? #{:logseq.property/query} (:db/ident m))
                                    (and (not page?) (contains? #{:block/alias} (: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
                                    ;; 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 (rum/react *property)
         property-key (rum/react *property-key)]
         property-key (rum/react *property-key)]
     [:div.ls-property-input.flex.flex-1.flex-row.items-center.flex-wrap.gap-1
     [: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
 (rum/defcs new-property < rum/reactive
   [state block opts]
   [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}}
     [:div.ls-new-property {:style {:margin-left 6 :margin-top 1}}
      [:a.fade-link.flex.jtrigger
      [:a.fade-link.flex.jtrigger
       {:tab-index 0
       {:tab-index 0
@@ -553,7 +586,7 @@
    :will-remount (fn [state]
    :will-remount (fn [state]
                    (let [block (db/entity (:db/id (::block state)))]
                    (let [block (db/entity (:db/id (::block state)))]
                      (assoc state ::classes (async-load-classes! block))))}
                      (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)
   (let [id (::id state)
         db-id (:db/id (::block state))
         db-id (:db/id (::block state))
         block (db/sub-block db-id)
         block (db/sub-block db-id)
@@ -563,7 +596,6 @@
                                                      (and (set? ids) (contains? ids (:block/uuid block))))))
                                                      (and (set? ids) (contains? ids (:block/uuid block))))))
         _ (doseq [class (::classes state)]
         _ (doseq [class (::classes state)]
             (db/sub-block (:db/id class)))
             (db/sub-block (:db/id class)))
-        page? (db/page? block)
         class? (ldb/class? block)
         class? (ldb/class? block)
         block-properties (:block/properties block)
         block-properties (:block/properties block)
         properties (cond
         properties (cond
@@ -652,20 +684,38 @@
                                      (when (and class? (nil? (:logseq.property.class/properties block)))
                                      (when (and class? (nil? (:logseq.property.class/properties block)))
                                        [[:logseq.property.class/properties nil]]))
                                        [[:logseq.property.class/properties nil]]))
                              remove-built-in-or-other-position-properties)]
                              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;
   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;
   margin-left: 7px;
 }
 }
 
 

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

@@ -96,7 +96,7 @@
                (let [toggle-fn #(do
                (let [toggle-fn #(do
                                   (when (fn? on-hide) (on-hide))
                                   (when (fn? on-hide) (on-hide))
                                   (shui/popup-hide! id))
                                   (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]
                      options (map (fn [class]
                                     {:label (:block/title class)
                                     {:label (:block/title class)
                                      :value (:block/uuid class)})
                                      :value (:block/uuid class)})

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

@@ -5,7 +5,6 @@
             [dommy.core :as d]
             [dommy.core :as d]
             [frontend.components.icon :as icon-component]
             [frontend.components.icon :as icon-component]
             [frontend.components.select :as select]
             [frontend.components.select :as select]
-            [frontend.components.title :as title]
             [frontend.config :as config]
             [frontend.config :as config]
             [frontend.date :as date]
             [frontend.date :as date]
             [frontend.db :as db]
             [frontend.db :as db]
@@ -34,7 +33,8 @@
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.shui.ui :as shui]
             [logseq.shui.ui :as shui]
             [promesa.core :as p]
             [promesa.core :as p]
-            [rum.core :as rum]))
+            [rum.core :as rum]
+            [clojure.set :as set]))
 
 
 (rum/defc property-empty-btn-value
 (rum/defc property-empty-btn-value
   [property & opts]
   [property & opts]
@@ -506,13 +506,16 @@
                              (when (and property-type (not= property-type :node))
                              (when (and property-type (not= property-type :node))
                                (if (= property-type :page)
                                (if (= property-type :page)
                                  (not (db/page? node))
                                  (not (db/page? node))
-                                 (not= property-type (some-> (:block/type node) keyword))))))
+                                 (not (contains? (ldb/get-entity-types node) property-type))))))
                        result)))))
                        result)))))
 
 
         options (map (fn [node]
         options (map (fn [node]
                        (let [id (or (:value node) (:db/id node))
                        (let [id (or (:value node) (:db/id node))
                              [header label] (if (integer? id)
                              [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)
                                                     node (or (db/entity id) node)
                                                     icon (get-node-icon node)
                                                     icon (get-node-icon node)
                                                     header (when-not (db/page? node)
                                                     header (when-not (db/page? node)
@@ -529,7 +532,10 @@
                                 :header header
                                 :header header
                                 :label-value (:block/title node)
                                 :label-value (:block/title node)
                                 :label label
                                 :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)
         classes' (remove (fn [class] (= :logseq.class/Root (:db/ident class))) classes)
         opts' (cond->
         opts' (cond->
                (merge
                (merge
@@ -547,7 +553,10 @@
                                               "Choose nodes"
                                               "Choose nodes"
                                               :else
                                               :else
                                               "Choose node")
                                               "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
                                                    false
                                                    true)
                                                    true)
                  :extract-chosen-fn :value
                  :extract-chosen-fn :value
@@ -815,6 +824,9 @@
        (= value :logseq.property/empty-placeholder)
        (= value :logseq.property/empty-placeholder)
        (property-empty-btn-value property)
        (property-empty-btn-value property)
 
 
+       closed-values?
+       (closed-value-item value opts)
+
        (or (ldb/page? value)
        (or (ldb/page? value)
            (and (seq (:block/tags value))
            (and (seq (:block/tags value))
                 ;; FIXME: page-cp should be renamed to node-cp and
                 ;; FIXME: page-cp should be renamed to node-cp and
@@ -832,9 +844,6 @@
        (when-let [reference (state/get-component :block/reference)]
        (when-let [reference (state/get-component :block/reference)]
          (reference {} (:block/uuid value)))
          (reference {} (:block/uuid value)))
 
 
-       closed-values?
-       (closed-value-item value opts)
-
        (de/entity? value)
        (de/entity? value)
        (when-some [content (str (db-property/property-value-content value))]
        (when-some [content (str (db-property/property-value-content value))]
          (inline-text-cp content))
          (inline-text-cp content))
@@ -966,7 +975,9 @@
   (let [type (get schema :type :default)
   (let [type (get schema :type :default)
         date? (= type :date)
         date? (= type :date)
         *el (rum/use-ref nil)
         *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!
     (rum/use-effect!
      (fn []
      (fn []
        (when editing?
        (when editing?
@@ -1002,7 +1013,7 @@
                            (do (some-> (rum/deref *el) (.click))
                            (do (some-> (rum/deref *el) (.click))
                                (util/stop e))
                                (util/stop e))
                            :dune))
                            :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])]
          (let [not-empty-value? (not= (map :db/ident items) [:logseq.property/empty-placeholder])]
            (if (and (seq items) not-empty-value?)
            (if (and (seq items) not-empty-value?)
              (concat
              (concat

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

@@ -227,7 +227,7 @@
         db-based? (config/db-based-graph? repo)]
         db-based? (config/db-based-graph? repo)]
     (rum/use-effect!
     (rum/use-effect!
      (fn []
      (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)))
          (set-values! result)))
      [])
      [])
     (let [items (->> values
     (let [items (->> values

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

@@ -307,8 +307,8 @@
         db-based? (config/db-based-graph? current-repo)
         db-based? (config/db-based-graph? current-repo)
         repos (sort-repos-with-metadata-local repos)
         repos (sort-repos-with-metadata-local repos)
         repos (distinct
         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)
         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
         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
                      [: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?
                         (if remotes-loading?
                           (ui/loading "")
                           (ui/loading "")
                           (shui/button
                           (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))))
         _remote? (and current-repo (:remote? (first (filter #(= current-repo (:url %)) repos))))
         _repo-name (when current-repo (db/get-repo-name current-repo))]
         _repo-name (when current-repo (db/get-repo-name current-repo))]
 
 
@@ -339,23 +339,23 @@
           (if hr
           (if hr
             (shui/dropdown-menu-separator)
             (shui/dropdown-menu-separator)
             (shui/dropdown-menu-item
             (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?)]))
      (repos-footer multiple-windows? db-based?)]))
 
 
 (rum/defcs repos-dropdown < rum/reactive
 (rum/defcs repos-dropdown < rum/reactive
-                            (rum/local false ::electron-multiple-windows?)
+  (rum/local false ::electron-multiple-windows?)
   [state & {:as opts}]
   [state & {:as opts}]
   (let [current-repo (state/sub :git/current-repo)
   (let [current-repo (state/sub :git/current-repo)
         login? (boolean (state/sub :auth/id-token))]
         login? (boolean (state/sub :auth/id-token))]
@@ -365,38 +365,38 @@
           db-based? (config/db-based-graph? current-repo)
           db-based? (config/db-based-graph? current-repo)
           repos (sort-repos-with-metadata-local repos)
           repos (sort-repos-with-metadata-local repos)
           repos (distinct
           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))))
       (let [remote? (and current-repo (:remote? (first (filter #(= current-repo (:url %)) repos))))
             repo-name (when current-repo (db/get-repo-name current-repo))
             repo-name (when current-repo (db/get-repo-name current-repo))
             short-repo-name (if current-repo
             short-repo-name (if current-repo
                               (db/get-short-repo-name repo-name)
                               (db/get-short-repo-name repo-name)
                               "Select a Graph")]
                               "Select a Graph")]
         (shui/trigger-as :a
         (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
 (rum/defcs graphs-selector < rum/reactive
   [_state]
   [_state]
@@ -413,10 +413,10 @@
      [:a.item.flex.items-center.gap-1.select-none
      [:a.item.flex.items-center.gap-1.select-none
       {:on-click (fn [^js e]
       {:on-click (fn [^js e]
                    (shui/popup-show! (.closest (.-target e) "a")
                    (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})]
       [:span.thumb (shui/tabler-icon (if remote? "cloud" (if db-based? "database" "folder")) {:size 16})]
       [:strong short-repo-name]
       [:strong short-repo-name]
       (shui/tabler-icon "selector" {:size 18})]]))
       (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])
     (let [lookup (if (integer? db-id) db-id [:block/uuid db-id])
           page (db/entity repo lookup)]
           page (db/entity repo lookup)]
       (if (ldb/page? page)
       (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)]]
           [:span.overflow-hidden.text-ellipsis (:block/title page)]]
          (page-cp repo (str (:block/uuid page)))]
          (page-cp repo (str (:block/uuid page)))]
         (block-with-breadcrumb repo page idx [repo db-id block-type] false)))
         (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;
     @apply opacity-100;
   }
   }
 }
 }
-
-.sidebar-panel-content {
-  @apply pt-3;
-}

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

@@ -29,7 +29,8 @@
               (when multiple-choices?
               (when multiple-choices?
                 (ui/checkbox {:checked (boolean (selected-choices (:value result)))
                 (ui/checkbox {:checked (boolean (selected-choices (:value result)))
                               :on-click (fn [e]
                               :on-click (fn [e]
-                                          (.preventDefault e))}))
+                                          (.preventDefault e))
+                              :disabled (:disabled? result)}))
               value]
               value]
              (when (and (map? result) (:id result))
              (when (and (map? result) (:id result))
                [:div.tip.flex
                [:div.tip.flex
@@ -67,7 +68,7 @@
    :will-unmount (fn [state]
    :will-unmount (fn [state]
                    (shui/dialog-close! :ls-select-modal)
                    (shui/dialog-close! :ls-select-modal)
                    state)}
                    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?
                  prompt-key input-default-placeholder close-modal?
                  extract-fn extract-chosen-fn host-opts on-input input-opts
                  extract-fn extract-chosen-fn host-opts on-input input-opts
                  item-cp transform-fn tap-*input-val
                  item-cp transform-fn tap-*input-val
@@ -138,7 +139,8 @@
                            [:div.item-results-wrap
                            [:div.item-results-wrap
                             (ui/auto-complete
                             (ui/auto-complete
                              search-result
                              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)))
                                                                (render-item result chosen? multiple-choices? *selected-choices)))
                               :class             "cp__select-results"
                               :class             "cp__select-results"
                               :on-chosen         (fn [raw-chosen e]
                               :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"
      {:variant "text"
       :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
       :class "h-8 !pl-4 !px-2 !py-0 hover:text-foreground w-full justify-start"
       :on-click #(column-toggle-sorting! column)}
       :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
        true
        (ui/icon "arrow-up")
        (ui/icon "arrow-up")
        false
        false
@@ -529,7 +529,6 @@
 (defn- get-property-values
 (defn- get-property-values
   [rows property]
   [rows property]
   (let [property-ident (:db/ident property)
   (let [property-ident (:db/ident property)
-        block-type? (= property-ident :block/type)
         values (->> (mapcat (fn [e] (let [e' (db/entity (:db/id e))
         values (->> (mapcat (fn [e] (let [e' (db/entity (:db/id e))
                                           v (get e' property-ident)]
                                           v (get e' property-ident)]
                                       (if (set? v) v #{v}))) rows)
                                       (if (set? v) v #{v}))) rows)
@@ -537,9 +536,8 @@
                     (distinct))]
                     (distinct))]
     (->>
     (->>
      (map (fn [e]
      (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)
           values)
      (sort-by :label))))
      (sort-by :label))))
 
 
@@ -607,7 +605,7 @@
                                  (do
                                  (do
                                    (shui/popup-hide!)
                                    (shui/popup-hide!)
                                    (let [property internal-property
                                    (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)
                                          filters' (if (seq filters)
                                                     (conj filters new-filter)
                                                     (conj filters new-filter)
                                                     [new-filter])]
                                                     [new-filter])]
@@ -685,15 +683,14 @@
     [:before :after]
     [:before :after]
     (concat
     (concat
      [:is :is-not]
      [: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
 (defn- get-filter-with-changed-operator
   [_property operator value]
   [_property operator value]
@@ -821,16 +818,11 @@
        {:class "!px-2 rounded-none border-r"
        {:class "!px-2 rounded-none border-r"
         :variant "ghost"
         :variant "ghost"
         :size :sm}
         :size :sm}
-       (let [block-type? (= (:db/ident property) :block/type)
-             value (cond
+       (let [value (cond
                      (uuid? value)
                      (uuid? value)
                      (db/entity [:block/uuid value])
                      (db/entity [:block/uuid value])
                      (and (coll? value) (every? uuid? value))
                      (and (coll? value) (every? uuid? value))
                      (set (map #(db/entity [:block/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
                      :else
                      value)]
                      value)]
          [:div.flex.flex-row.items-center.gap-1.text-xs
          [:div.flex.flex-row.items-center.gap-1.text-xs
@@ -1012,15 +1004,15 @@
 (rum/defc new-record-button < rum/static
 (rum/defc new-record-button < rum/static
   [table view-entity]
   [table view-entity]
   (let [asset? (and (:logseq.property/built-in? view-entity)
   (let [asset? (and (:logseq.property/built-in? view-entity)
-                 (= (:block/name view-entity) "asset"))]
+                    (= (:block/name view-entity) "asset"))]
     (ui/tooltip
     (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
 (rum/defc add-new-row < rum/static
   [table]
   [table]
@@ -1084,8 +1076,8 @@
         *rows-wrap (rum/use-ref nil)]
         *rows-wrap (rum/use-ref nil)]
 
 
     (rum/use-effect!
     (rum/use-effect!
-      (fn [] (set-ready? true))
-      [])
+     (fn [] (set-ready? true))
+     [])
 
 
     (shui/table
     (shui/table
      (let [columns' (:columns table)
      (let [columns' (:columns table)
@@ -1097,17 +1089,17 @@
            (table-header table columns' option selected-rows)
            (table-header table columns' option selected-rows)
 
 
            (ui/virtualized-list
            (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!
            (when add-new-object!
              (shui/table-footer (add-new-row table)))])]))))
              (shui/table-footer (add-new-row table)))])]))))
@@ -1245,39 +1237,42 @@
 
 
     [:div.flex.flex-col.gap-2.grid
     [:div.flex.flex-col.gap-2.grid
      {:ref *view-ref}
      {: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
 (rum/defcs view
   "Provides a view for data like query results and tagged objects, multiple
   "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
 (defn tt
   [& keys]
   [& keys]
   (some->
   (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 []
 (defn- fetch-local-language []
   (.. js/window -navigator -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?]
   [graph & {:keys [remove-built-in-property? remove-non-queryable-built-in-property?]
             :or {remove-built-in-property? true
             :or {remove-built-in-property? true
                  remove-non-queryable-built-in-property? false}}]
                  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))))
                     (map (fn [datom] (db/entity (:e datom))))
                     (sort-by (juxt ldb/built-in? :block/title)))]
                     (sort-by (juxt ldb/built-in? :block/title)))]
     (cond->> result
     (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?
 (defn page-exists?
   "Whether a page exists."
   "Whether a page exists."
-  [page-name type]
+  [page-name tags]
   (let [repo (state/get-current-repo)]
   (let [repo (state/get-current-repo)]
     (when-let [db (conn/get-db 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?
 (defn page-empty?
   "Whether a page is empty. Does it has a non-page block?
   "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
 (defn get-journals-length
   []
   []
   (let [today (date-time-util/date->int (js/Date.))]
   (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
 (defn get-latest-journals
   ([n]
   ([n]
@@ -749,17 +758,29 @@ independent of format as format specific heading characters are stripped"
 
 
 (defn get-all-whiteboards
 (defn get-all-whiteboards
   [repo]
   [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
 (defn get-whiteboard-id-nonces
   [repo page-id]
   [repo page-id]
@@ -777,16 +798,27 @@ independent of format as format specific heading characters are stripped"
                     :nonce (:nonce shape)}))))))
                     :nonce (:nonce shape)}))))))
 
 
 (defn get-all-classes
 (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)
   (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]
                      (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?
     (if except-root-class?
       (keep (fn [e] (when-not (= :logseq.class/Root (:db/ident e)) e)) classes)
       (keep (fn [e] (when-not (= :logseq.class/Root (:db/ident e)) e)) classes)
       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
 (defn get-structured-children
   [repo eid]
   [repo eid]
   (->>
   (->>
@@ -802,13 +834,15 @@ independent of format as format specific heading characters are stripped"
 (defn get-class-objects
 (defn get-class-objects
   [repo class-id]
   [repo class-id]
   (when-let [class (db-utils/entity 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
 (defn sub-class-objects
   [repo class-id]
   [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]
               (rules/extract-rules rules/db-query-dsl-rules [:has-property-or-default-value]
                                    {:deps rules/rules-dependencies})
                                    {:deps rules/rules-dependencies})
               (:db/ident property))
               (:db/ident property))
-         (map #(db-utils/entity repo %)))))
+         (map #(db-utils/entity repo %))
+         (remove ldb/hidden?))))
 
 
 (defn get-all-namespace-relation
 (defn get-all-namespace-relation
   [repo]
   [repo]
@@ -849,18 +884,30 @@ independent of format as format specific heading characters are stripped"
 (defn get-pages-relation
 (defn get-pages-relation
   [repo with-journal?]
   [repo with-journal?]
   (when-let [db (conn/get-db repo)]
   (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
 (defn get-namespace-pages
   "Accepts both sanitized and unsanitized namespaces"
   "Accepts both sanitized and unsanitized namespaces"

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

@@ -323,7 +323,7 @@
     (or (some->> (name property-name)
     (or (some->> (name property-name)
                  (db-utils/q '[:find [(pull ?b [:db/ident]) ...]
                  (db-utils/q '[:find [(pull ?b [:db/ident]) ...]
                                :in $ ?title
                                :in $ ?title
-                               :where [?b :block/type "property"] [?b :block/title ?title]])
+                               :where [?b :block/tags :logseq.class/Property] [?b :block/title ?title]])
                  first
                  first
                  :db/ident)
                  :db/ident)
         ;; Don't return nil as that incorrectly matches all properties
         ;; 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]
    [frontend.handler.property.util :as pu]
    [dommy.core :as dom]
    [dommy.core :as dom]
    [goog.object :as gobj]
    [goog.object :as gobj]
-   [promesa.core :as p]))
+   [promesa.core :as p]
+   [datascript.impl.entity :as de]))
 
 
 ;;  Fns
 ;;  Fns
 
 
@@ -188,6 +189,32 @@
     (-> (property-util/remove-built-in-properties format content)
     (-> (property-util/remove-built-in-properties format content)
         (drawer/remove-logbook))))
         (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!
 (defn edit-block!
   [block pos & {:keys [_container-id custom-content tail-len save-code-editor?]
   [block pos & {:keys [_container-id custom-content tail-len save-code-editor?]
                 :or {tail-len 0
                 :or {tail-len 0

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

@@ -4,15 +4,16 @@
             [frontend.db :as db]
             [frontend.db :as db]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.common.page :as page-common-handler]
             [frontend.handler.common.page :as page-common-handler]
+            [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.notification :as notification]
             [frontend.state :as state]
             [frontend.state :as state]
-            [frontend.modules.outliner.ui :as ui-outliner-tx]
             [logseq.outliner.validate :as outliner-validate]
             [logseq.outliner.validate :as outliner-validate]
             [logseq.db.frontend.class :as db-class]
             [logseq.db.frontend.class :as db-class]
             [logseq.common.util :as common-util]
             [logseq.common.util :as common-util]
             [logseq.common.util.page-ref :as page-ref]
             [logseq.common.util.page-ref :as page-ref]
             [datascript.impl.entity :as de]
             [datascript.impl.entity :as de]
-            [promesa.core :as p]))
+            [promesa.core :as p]
+            [logseq.db]))
 
 
 (defn- valid-tag?
 (defn- valid-tag?
   "Returns a boolean indicating whether the new tag passes all valid checks.
   "Returns a boolean indicating whether the new tag passes all valid checks.
@@ -32,27 +33,23 @@
         (throw e)))))
         (throw e)))))
 
 
 (defn add-tag [repo block-id tag-entity]
 (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!
 (defn convert-to-tag!
   [page-entity]
   [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)
     (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!
 (defn <create-class!
   "Creates a class page and provides class-specific error handling"
   "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]
             [frontend.storage :as storage]
             [logseq.graph-parser.db :as gp-db]
             [logseq.graph-parser.db :as gp-db]
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [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
 (defn- build-links
   [links]
   [links]
@@ -45,7 +44,7 @@
                    size (int (* 8 (max 1.0 (js/Math.cbrt n))))]
                    size (int (* 8 (max 1.0 (js/Math.cbrt n))))]
                (cond->
                (cond->
                 {:id (str (:db/id p))
                 {:id (str (:db/id p))
-                 :label (title/block-unique-title p)
+                 :label page-title
                  :size size
                  :size size
                  :color color
                  :color color
                  :block/created-at (:block/created-at p)}
                  :block/created-at (:block/created-at p)}
@@ -72,8 +71,8 @@
 (defn- normalize-page-name
 (defn- normalize-page-name
   [{:keys [nodes links]}]
   [{:keys [nodes links]}]
   (let [nodes' (->> (remove-uuids-and-files! nodes)
   (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'
     {:nodes nodes'
      :links links}))
      :links links}))
 
 
@@ -207,15 +206,15 @@
   (let [search-nodes (fn [forward?]
   (let [search-nodes (fn [forward?]
                        (let [links (group-by (if forward? :source :target) links)]
                        (let [links (group-by (if forward? :source :target) links)]
                          (loop [nodes nodes
                          (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 (concat (search-nodes true) (search-nodes false))
         nodes (set nodes)]
         nodes (set nodes)]
     (update graph :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)))
   (when (and page (state/enable-journals? (state/get-current-repo)))
     (p/do!
     (p/do!
      (db-async/<get-block (state/get-current-repo) page :children? false)
      (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
        (route-handler/redirect! {:to          :page
                                  :path-params {:name page}})
                                  :path-params {:name page}})
        (page-handler/<create! page)))))
        (page-handler/<create! page)))))

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

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

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

@@ -69,7 +69,7 @@
     {:db/id (:db/id page-entity)
     {:db/id (:db/id page-entity)
      :block/title page-name
      :block/title page-name
      :block/name (util/page-name-sanity-lc page-name)
      :block/name (util/page-name-sanity-lc page-name)
-     :block/type "whiteboard"
+     :block/tags :logseq.class/Whiteboard
      :block/format :markdown
      :block/format :markdown
      :logseq.property/ls-type :whiteboard-page
      :logseq.property/ls-type :whiteboard-page
      :logseq.property.tldraw/page tldraw-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.transact]
                      [frontend.db.conn]
                      [frontend.db.conn]
                      [logseq.outliner.op]
                      [logseq.outliner.op]
-                     [frontend.modules.outliner.op])))
+                     [frontend.modules.outliner.op]
+                     [logseq.db])))
 
 
 (defmacro transact!
 (defmacro transact!
   [opts & body]
   [opts & body]

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

@@ -511,42 +511,53 @@
            empty-placeholder
            empty-placeholder
            item-render
            item-render
            class
            class
-           header]}]
+           header
+           grouped?]}]
   (let [*current-idx (get state ::current-idx)
   (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}
     [:div#ui__ac {:class class}
      (if (seq matched)
      (if (seq matched)
        [:div#ui__ac-inner.hide-scrollbar
        [:div#ui__ac-inner.hide-scrollbar
         (when header header)
         (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
        (when empty-placeholder
          empty-placeholder))]))
          empty-placeholder))]))
 
 
@@ -593,28 +604,27 @@
   (rum/local false ::control?)
   (rum/local false ::control?)
   [state {:keys [on-pointer-down header title-trigger? collapsed?]}]
   [state {:keys [on-pointer-down header title-trigger? collapsed?]}]
   (let [control? (get state ::control?)]
   (let [control? (get state ::control?)]
-    [:div.content
+    [:div.ls-foldable-title.content
      [:div.flex-1.flex-row.foldable-title (cond->
      [:div.flex-1.flex-row.foldable-title (cond->
                                            {:on-mouse-over #(reset! control? true)
                                            {:on-mouse-over #(reset! control? true)
                                             :on-mouse-out  #(reset! control? false)}
                                             :on-mouse-out  #(reset! control? false)}
                                             title-trigger?
                                             title-trigger?
                                             (assoc :on-pointer-down on-pointer-down
                                             (assoc :on-pointer-down on-pointer-down
                                                    :class "cursor"))
                                                    :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]
        {:on-click (fn [^js e]
                     (let [^js target (.-target e)]
                     (let [^js target (.-target e)]
                       (when (some-> target (.closest ".as-toggle"))
                       (when (some-> target (.closest ".as-toggle"))
                         (reset! collapsed? (not @collapsed?)))))}
                         (reset! collapsed? (not @collapsed?)))))}
        (when-not (mobile-util/native-platform?)
        (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)
        (if (fn? header)
          (header @collapsed?)
          (header @collapsed?)
          header)]]]))
          header)]]]))

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

@@ -329,4 +329,8 @@ input[type='range'] {
   .as-toggle {
   .as-toggle {
     @apply opacity-60 cursor-pointer select-none active:opacity-50;
     @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'})))))
                 :block/title title'})))))
      datoms)))
      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
 (defn- deprecate-logseq-user-ns
   [conn _search-db]
   [conn _search-db]
   (let [db @conn]
   (let [db @conn]
@@ -514,9 +542,10 @@
    [47 {:fix replace-hidden-type-with-schema}]
    [47 {:fix replace-hidden-type-with-schema}]
    [48 {:properties [:logseq.property/default-value :logseq.property/scalar-default-value]}]
    [48 {:properties [:logseq.property/default-value :logseq.property/scalar-default-value]}]
    [49 {:fix replace-special-id-ref-with-id-ref}]
    [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))]
 (let [max-schema-version (apply max (map first schema-version->updates))]
   (assert (<= db-schema/version max-schema-version))
   (assert (<= db-schema/version max-schema-version))
@@ -534,17 +563,20 @@
                             (into {})
                             (into {})
                             sqlite-create-graph/build-initial-properties*
                             sqlite-create-graph/build-initial-properties*
                             (map (fn [b] (assoc b :logseq.property/built-in? true))))
                             (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 {})
                          (into {})
                          (#(sqlite-create-graph/build-initial-classes* % (zipmap properties properties)))
                          (#(sqlite-create-graph/build-initial-classes* % (zipmap properties properties)))
                          (map (fn [b] (assoc b :logseq.property/built-in? true))))
                          (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)
         fixes (when (fn? fix)
                 (fix conn search-db))
                 (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
         tx-data' (concat
                   [(sqlite-util/kv :logseq.kv/schema-version version)]
                   [(sqlite-util/kv :logseq.kv/schema-version version)]
                   tx-data)]
                   tx-data)]
@@ -555,30 +587,31 @@
   "Migrate 'frontend' datascript schema and data. To add a new migration,
   "Migrate 'frontend' datascript schema and data. To add a new migration,
   add an entry to schema-version->updates and bump db-schema/version"
   add an entry to schema-version->updates and bump db-schema/version"
   [conn search-db]
   [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
       ;; 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
 ;; Backend migrations
 ;; ==================
 ;; ==================

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

@@ -318,7 +318,7 @@
 
 
         (when-not db-based?
         (when-not db-based?
           (try
           (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)))
               (ldb/transact! conn (sqlite-create-graph/build-initial-views)))
             (catch :default _e)))
             (catch :default _e)))
 
 

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

@@ -6,8 +6,7 @@
             [logseq.graph-parser.property :as gp-property]
             [logseq.graph-parser.property :as gp-property]
             [logseq.outliner.tree :as otree]
             [logseq.outliner.tree :as otree]
             [cljs-bean.core :as bean]
             [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
 (defn- safe-keywordize
   [block]
   [block]
@@ -64,23 +63,17 @@
                          (:keys result)))))
                          (:keys result)))))
            (group-by first)
            (group-by first)
            (mapcat (fn [[_id col]]
            (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)
                                          (when (= a :db/ident)
-                                           v)) col)]
+                                           v)) col)
+                           journal (some (fn [[_e a v _t]]
+                                           (when (= a :block/journal-day)
+                                             v)) col)]
                        (map
                        (map
                         (fn [[e a v t]]
                         (fn [[e a v t]]
                           (cond
                           (cond
                             (and (contains? #{:block/title :block/name} a)
                             (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]
                             [e a (str "debug " e) t]
 
 
                             (= a :block/uuid)
                             (= 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.
    * :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.
    * :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
    * :tags                     - tag uuids that are added to :block/tags
    * :persist-op?              - when true, add an update-page op
    * :persist-op?              - when true, add an update-page op
    * :properties               - properties to add to the page
    * :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.db.sqlite.util :as sqlite-util]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.block :as gp-block]
             [logseq.graph-parser.text :as text]
             [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]}]
 (defn- build-page-tx [conn properties page {:keys [whiteboard? class? tags]}]
   (when (:block/uuid page)
   (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
           property-vals-tx-m
           ;; Builds property values for built-in properties like logseq.property.pdf/file
           ;; Builds property values for built-in properties like logseq.property.pdf/file
           (db-property-build/build-property-values-tx-m
           (db-property-build/build-property-values-tx-m
@@ -95,10 +95,12 @@
 
 
 (defn- split-namespace-pages
 (defn- split-namespace-pages
   [db page date-formatter]
   [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)
              parts (->> (string/split title ns-util/parent-re)
                         (map string/trim)
                         (map string/trim)
                         (remove string/blank?))
                         (remove string/blank?))
@@ -168,15 +170,24 @@
   (let [db @conn
   (let [db @conn
         date-formatter (:logseq.property.journal/title-format (d/entity db :logseq.class/Journal))
         date-formatter (:logseq.property.journal/title-format (d/entity db :logseq.class/Journal))
         title (sanitize-title title*)
         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
       (let [format    :markdown
             page      (-> (gp-block/page-name->map title @conn true date-formatter
             page      (-> (gp-block/page-name->map title @conn true date-formatter
                                                    {:class? class?
                                                    {:class? class?
@@ -189,9 +200,12 @@
                              (let [pages (split-namespace-pages db page date-formatter)]
                              (let [pages (split-namespace-pages db page date-formatter)]
                                [(last pages) (butlast pages)])
                                [(last pages) (butlast pages)])
                              [page nil])]
                              [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 '/'
           ;; 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})
             (outliner-validate/validate-page-title-characters (str (:block/title page)) {:node page})
             (doseq [parent parents]
             (doseq [parent parents]
               (outliner-validate/validate-page-title-characters (str (:block/title parent)) {:node parent})))
               (outliner-validate/validate-page-title-characters (str (:block/title parent)) {:node parent})))
@@ -205,7 +219,8 @@
                                       page-txs)
                                       page-txs)
                                  (build-first-block-tx (:block/uuid (first page-txs)) format))
                                  (build-first-block-tx (:block/uuid (first page-txs)) format))
                 txs      (concat
                 txs      (concat
-                          parents
+                          ;; transact doesn't support entities
+                          (remove de/entity? parents)
                           page-txs
                           page-txs
                           first-block-tx)]
                           first-block-tx)]
             (when (seq txs)
             (when (seq txs)

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

@@ -22,7 +22,7 @@
 
 
 (def ^:private watched-attrs
 (def ^:private watched-attrs
   #{:block/title :block/created-at :block/updated-at :block/alias
   #{: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
     :property/schema.classes :property.value/content
     :db/index :db/valueType :db/cardinality})
     :db/index :db/valueType :db/cardinality})
 
 

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

@@ -366,7 +366,6 @@
     :block/updated-at
     :block/updated-at
     :block/created-at
     :block/created-at
     :block/alias
     :block/alias
-    :block/type
     :block/schema
     :block/schema
     :block/tags
     :block/tags
     :block/link
     :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)
   (drop-tables-and-triggers! db)
   (create-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
 (comment
   (defn- get-db-properties-str
   (defn- get-db-properties-str
     "Similar to db-pu/readable-properties but with a focus on making property values searchable"
     "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.db :as db]
             [frontend.test.helper :as test-helper]
             [frontend.test.helper :as test-helper]
             [datascript.core :as d]
             [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)
 (def repo test-helper/test-db-name-db-version)
 
 
@@ -27,7 +29,9 @@
         _ (test-helper/create-page! "class2" opts)]
         _ (test-helper/create-page! "class2" opts)]
     (is (= (set
     (is (= (set
             (concat
             (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"]))
              ["class1" "class2"]))
            (set (map :block/title (model/get-all-classes repo)))))))
            (set (map :block/title (model/get-all-classes repo)))))))
 
 
@@ -51,19 +55,16 @@
               (:db/id (db/entity [:block/uuid sbid]))])))))
               (:db/id (db/entity [:block/uuid sbid]))])))))
 
 
 (deftest get-classes-with-property-test
 (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
 (deftest hidden-page-test
   (let [opts {:redirect? false :create-first-block? false}
   (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)
   (let [conn (db-test/create-conn)
         _ (worker-db-page/create! conn "movie" {:class? true})
         _ (worker-db-page/create! conn "movie" {:class? true})
         _ (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 a class")
     (is (ldb/class? Movie-class) "Creates another class with a different case sensitive name")
     (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")
             "Child class with new parent has correct parents")
 
 
         (worker-db-page/create! conn "foo/class1/baz3" {:split-namespace? true})
         (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")))
             "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"
     (testing "Child pages with same name and different parents"
@@ -79,10 +78,23 @@
   (let [conn (db-test/create-conn)
   (let [conn (db-test/create-conn)
         [_ page-uuid] (worker-db-page/create! conn "fooz" {})]
         [_ page-uuid] (worker-db-page/create! conn "fooz" {})]
     (is (= "fooz" (:block/title (d/entity @conn [:block/uuid page-uuid])))
     (is (= "fooz" (:block/title (d/entity @conn [:block/uuid page-uuid])))
-        "Valid page created")
+        "Page created correctly")
 
 
     (is (thrown-with-msg?
     (is (thrown-with-msg?
          js/Error
          js/Error
          #"can't include \"/"
          #"can't include \"/"
          (worker-db-page/create! conn "foo/bar" {}))
          (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.db-listener :as subject]
             [frontend.worker.rtc.fixture :as r.fixture]
             [frontend.worker.rtc.fixture :as r.fixture]
             [frontend.worker.state :as worker-state]
             [frontend.worker.state :as worker-state]
-            [logseq.db.frontend.schema :as db-schema]
             [logseq.outliner.batch-tx :as batch-tx]
             [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
 (t/use-fixtures :each
   test-helper/db-based-start-and-destroy-db-map-fixture
   test-helper/db-based-start-and-destroy-db-map-fixture
   r.fixture/listen-test-db-to-gen-rtc-ops-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
 (defn- tx-data=>e->a->add?->v->t
   [tx-data]
   [tx-data]
   (let [datom-vec-coll (map vec tx-data)
   (let [datom-vec-coll (map vec tx-data)
@@ -27,11 +25,11 @@
 
 
 (deftest entity-datoms=>ops-test
 (deftest entity-datoms=>ops-test
   (testing "remove whiteboard page-block"
   (testing "remove whiteboard page-block"
-    (let [conn (d/conn-from-db empty-db)
+    (let [conn (db-test/create-conn)
           block-uuid (random-uuid)
           block-uuid (random-uuid)
           _create-whiteboard-page-block
           _create-whiteboard-page-block
           (d/transact! conn [{:block/uuid block-uuid
           (d/transact! conn [{:block/uuid block-uuid
-                              :block/type "whiteboard"
+                              :block/tags :logseq.class/Whiteboard
                               :block/name "block-name"
                               :block/name "block-name"
                               :block/title "BLOCK-NAME"}])
                               :block/title "BLOCK-NAME"}])
           remove-whiteboard-page-block
           remove-whiteboard-page-block
@@ -44,20 +42,20 @@
              (map (fn [[op-type _t op-value]] [op-type op-value]) r)))))
              (map (fn [[op-type _t op-value]] [op-type op-value]) r)))))
 
 
   (testing "update-schema op"
   (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)
           {:keys [db-before db-after tx-data]} (d/transact! conn tx-data)
           ops (#'subject/entity-datoms=>ops db-before db-after
           ops (#'subject/entity-datoms=>ops db-before db-after
                                             (tx-data=>e->a->add?->v->t tx-data)
                                             (tx-data=>e->a->add?->v->t tx-data)
@@ -72,10 +70,11 @@
                        [:block/updated-at "[\"~#'\",1716882111476]"]
                        [:block/updated-at "[\"~#'\",1716882111476]"]
                        [:block/created-at "[\"~#'\",1716882111476]"]
                        [:block/created-at "[\"~#'\",1716882111476]"]
                        [:block/schema "[\"^ \",\"~:type\",\"~:number\"]"]
                        [:block/schema "[\"^ \",\"~:type\",\"~:number\"]"]
+                       [:block/tags #uuid "00000002-1038-7670-4800-000000000000"]
                        [:block/title "[\"~#'\",\"qqq\"]"]
                        [:block/title "[\"~#'\",\"qqq\"]"]
                        [:db/cardinality "[\"~#'\",\"~:db.cardinality/one\"]"]
                        [:db/cardinality "[\"~#'\",\"~:db.cardinality/one\"]"]
                        ;; [:db/ident "[\"~#'\",\"~:user.property/qqq\"]"]
                        ;; [:db/ident "[\"~#'\",\"~:user.property/qqq\"]"]
-                       [:block/type "[\"~#'\",\"property\"]"]]}]]
+                       ]}]]
            (map (fn [[op-type _t op-value]]
            (map (fn [[op-type _t op-value]]
                   [op-type (cond-> op-value
                   [op-type (cond-> op-value
                              (:av-coll op-value)
                              (:av-coll op-value)
@@ -83,16 +82,16 @@
                 ops)))))
                 ops)))))
 
 
   (testing "create user-class"
   (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)
           {:keys [db-before db-after tx-data]} (d/transact! conn tx-data)
           ops (#'subject/entity-datoms=>ops db-before db-after
           ops (#'subject/entity-datoms=>ops db-before db-after
                                             (tx-data=>e->a->add?->v->t tx-data)
                                             (tx-data=>e->a->add?->v->t tx-data)
@@ -103,9 +102,9 @@
                       :av-coll
                       :av-coll
                       [[:block/updated-at "[\"~#'\",1720019497643]"]
                       [[:block/updated-at "[\"~#'\",1720019497643]"]
                        [:block/created-at "[\"~#'\",1720019497643]"]
                        [:block/created-at "[\"~#'\",1720019497643]"]
+                       [:block/tags #uuid "00000002-5389-0208-3000-000000000000"]
                        [:block/title "[\"~#'\",\"zzz\"]"]
                        [: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
                        ;;1. shouldn't have :db/ident, :db/ident is special, will be handled later
                        ]}]]
                        ]}]]
            (map (fn [[op-type _t op-value]]
            (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
 (deftest remote-op-value->tx-data-test
   (let [[block-uuid ref-uuid1 ref-uuid2] (repeatedly random-uuid)
   (let [[block-uuid ref-uuid1 ref-uuid2] (repeatedly random-uuid)
         db (d/db-with (d/empty-db db-schema/schema-for-db-based-graph)
         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"
     (testing ":block/title"
       (let [db (d/db-with db [{:block/uuid block-uuid
       (let [db (d/db-with db [{:block/uuid block-uuid
                                :block/title "local-content"}])
                                :block/title "local-content"}])

+ 5 - 5
static/yarn.lock

@@ -1831,7 +1831,7 @@ [email protected]:
     electron-log "^4.2.3"
     electron-log "^4.2.3"
     node-addon-api "^2.0.0"
     node-addon-api "^2.0.0"
 
 
[email protected]:
+electron-devtools-installer@^3.2.0:
   version "3.2.0"
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-3.2.0.tgz#acc48d24eb7033fe5af284a19667e73b78d406d0"
   resolved "https://registry.yarnpkg.com/electron-devtools-installer/-/electron-devtools-installer-3.2.0.tgz#acc48d24eb7033fe5af284a19667e73b78d406d0"
   integrity sha512-t3UczsYugm4OAbqvdImMCImIMVdFzJAHgbwHpkl5jmfu1izVgUcP/mnrPqJIpEeCK1uZGpt+yHgWEN+9EwoYhQ==
   integrity sha512-t3UczsYugm4OAbqvdImMCImIMVdFzJAHgbwHpkl5jmfu1izVgUcP/mnrPqJIpEeCK1uZGpt+yHgWEN+9EwoYhQ==
@@ -4626,13 +4626,13 @@ stream-buffers@~2.2.0:
     strip-ansi "^6.0.1"
     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:
 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:
   dependencies:
     emoji-regex "^8.0.0"
     emoji-regex "^8.0.0"
     is-fullwidth-code-point "^3.0.0"
     is-fullwidth-code-point "^3.0.0"
-    strip-ansi "^6.0.1"
+    strip-ansi "^6.0.0"
 
 
 string_decoder@^1.1.1:
 string_decoder@^1.1.1:
   version "1.3.0"
   version "1.3.0"