Sfoglia il codice sorgente

Merge branch 'feat/db' into feat/tables

Tienson Qin 1 anno fa
parent
commit
31305e1904
35 ha cambiato i file con 494 aggiunte e 324 eliminazioni
  1. 1 1
      deps/graph-parser/script/db_import.cljs
  2. 94 91
      deps/graph-parser/src/logseq/graph_parser/exporter.cljs
  3. 114 29
      deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs
  4. 1 0
      deps/graph-parser/test/resources/exporter-test-graph/.gitignore
  5. 2 0
      deps/graph-parser/test/resources/exporter-test-graph/journals/2024_02_07.md
  6. 9 0
      deps/graph-parser/test/resources/exporter-test-graph/journals/2024_02_14.md
  7. 6 1
      deps/graph-parser/test/resources/exporter-test-graph/journals/2024_04_01.md
  8. 3 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/Interstellar.md
  9. 1 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/contents.md
  10. 4 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/icon page.md
  11. 3 0
      deps/graph-parser/test/resources/exporter-test-graph/pages/new page.md
  12. 1 1
      deps/outliner/src/logseq/outliner/property.cljs
  13. 2 2
      src/main/frontend/components/all_pages.cljs
  14. 1 1
      src/main/frontend/components/block.cljs
  15. 3 1
      src/main/frontend/components/file_based/hierarchy.cljs
  16. 5 5
      src/main/frontend/components/file_sync.cljs
  17. 3 1
      src/main/frontend/components/header.cljs
  18. 2 2
      src/main/frontend/components/objects.cljs
  19. 55 47
      src/main/frontend/components/page.cljs
  20. 33 61
      src/main/frontend/components/property.cljs
  21. 1 2
      src/main/frontend/components/property/value.cljs
  22. 47 38
      src/main/frontend/components/server.cljs
  23. 3 1
      src/main/frontend/components/settings.cljs
  24. 9 8
      src/main/frontend/components/whiteboard.cljs
  25. 6 2
      src/main/frontend/db/utils.cljs
  26. 1 0
      src/main/frontend/handler/db_based/recent.cljs
  27. 6 5
      src/main/frontend/handler/events.cljs
  28. 2 3
      src/main/frontend/handler/file_based/property/util.cljs
  29. 1 2
      src/main/frontend/worker/react.cljs
  30. 1 0
      src/main/frontend/worker/rtc/client.cljs
  31. 1 10
      src/main/frontend/worker/rtc/const.cljs
  32. 9 3
      src/main/frontend/worker/rtc/db_listener.cljs
  33. 1 2
      src/test/frontend/db/db_based_model_test.cljs
  34. 31 3
      src/test/frontend/worker/rtc/client_test.cljs
  35. 32 2
      src/test/frontend/worker/rtc/db_listener_test.cljs

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

@@ -92,7 +92,7 @@
 (defn- import-files-to-db
   "Import specific doc files for dev purposes"
   [file conn {:keys [files] :as options}]
-  (let [doc-options (gp-exporter/build-doc-options conn {:macros {}} (merge options default-export-options))
+  (let [doc-options (gp-exporter/build-doc-options {:macros {}} (merge options default-export-options))
         files' (mapv #(hash-map :path %)
                      (into [file] (map resolve-path files)))]
     (gp-exporter/export-doc-files conn files' <read-file doc-options)))

+ 94 - 91
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -46,42 +46,49 @@
 (defn- convert-tag-to-class
   "Converts a tag block with class or returns nil if this tag should be removed
    because it has been moved"
-  [tag-block tag-classes]
+  [db tag-block tag-classes]
   (if-let [new-class (:block.temp/new-class tag-block)]
     (sqlite-util/build-new-class
      {:block/original-name new-class
       :block/name (common-util/page-name-sanity-lc new-class)})
     (when (contains? tag-classes (:block/name tag-block))
-      (-> tag-block
-          add-missing-timestamps
-          ;; don't use build-new-class b/c of timestamps
-          (merge {:block/format :markdown
-                  :block/type "class"})))))
+      (if-let [existing-tag-uuid (first
+                                  (d/q '[:find [?uuid ...]
+                                         :in $ ?name
+                                         :where [?b :block/uuid ?uuid] [?b :block/type "class"] [?b :block/name ?name]]
+                                       db
+                                       (:block/name tag-block)))]
+        [:block/uuid existing-tag-uuid]
+        ;; Creates or updates page within same tx
+        (-> (db-class/build-new-class db tag-block)
+            ;; 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]))))))))
+
+(defn- get-page-uuid [page-names-to-uuids page-name]
+  (or (get page-names-to-uuids page-name)
+      (throw (ex-info (str "No uuid found for page name " (pr-str page-name))
+                      {:page-name page-name}))))
 
 (defn- update-page-tags
-  [block tag-classes names-uuids page-tags-uuid]
+  [block db tag-classes page-names-to-uuids]
   (if (seq (:block/tags block))
     (let [page-tags (->> (:block/tags block)
                          (remove #(or (:block.temp/new-class %) (contains? tag-classes (:block/name %))))
-                         (map #(or (get names-uuids (:block/name %))
-                                   (throw (ex-info (str "No uuid found for tag " (pr-str (:block/name %)))
-                                                   {:tag %}))))
+                         (map #(vector :block/uuid (get-page-uuid page-names-to-uuids (:block/name %))))
                          set)]
       (cond-> block
         true
         (update :block/tags
                 (fn [tags]
-                  (keep #(convert-tag-to-class % tag-classes) tags)))
+                  (keep #(convert-tag-to-class db % tag-classes) tags)))
         (seq page-tags)
-        (update :block/properties merge {page-tags-uuid page-tags})))
+        (merge {:logseq.property/page-tags page-tags})))
     block))
 
 (defn- add-uuid-to-page-map [m page-names-to-uuids]
-  (assoc m
-         :block/uuid
-         (or (get page-names-to-uuids (:block/name m))
-             (throw (ex-info (str "No uuid found for page " (pr-str (:block/name m)))
-                             {:page m})))))
+  (assoc m :block/uuid (get-page-uuid page-names-to-uuids (:block/name m))))
 
 (defn- content-without-tags-ignore-case
   "Ignore case because tags in content can have any case and still have a valid ref"
@@ -97,7 +104,7 @@
    (string/trim)))
 
 (defn- update-block-tags
-  [block tag-classes page-names-to-uuids]
+  [block db tag-classes page-names-to-uuids]
   (if (seq (:block/tags block))
     (let [original-tags (remove :block.temp/new-class (:block/tags block))]
       (-> block
@@ -113,12 +120,12 @@
                        (map #(add-uuid-to-page-map % page-names-to-uuids))))
           (update :block/tags
                   (fn [tags]
-                    (keep #(convert-tag-to-class % tag-classes) tags)))))
+                    (keep #(convert-tag-to-class db % tag-classes) tags)))))
     block))
 
 (defn- update-block-marker
   "If a block has a marker, convert it to a task object"
-  [block db {:keys [log-fn]}]
+  [block {:keys [log-fn]}]
   (if-let [marker (:block/marker block)]
     (let [old-to-new {"TODO" :logseq.task/status.todo
                       "LATER" :logseq.task/status.todo
@@ -130,14 +137,12 @@
                       "WAITING" :logseq.task/status.backlog
                       "CANCELED" :logseq.task/status.canceled
                       "CANCELLED" :logseq.task/status.canceled}
-          status-prop (:block/uuid (d/entity db :logseq.task/status))
           status-ident (or (old-to-new marker)
                            (do
                              (log-fn :invalid-todo (str (pr-str marker) " is not a valid marker so setting it to TODO"))
-                             :logseq.task/status.todo))
-          status-value (:block/uuid (d/entity db status-ident))]
+                             :logseq.task/status.todo))]
       (-> block
-          (update :block/properties assoc status-prop status-value)
+          (assoc :logseq.task/status status-ident)
           (update :block/content string/replace-first (re-pattern (str marker "\\s*")) "")
           (update :block/tags (fnil conj []) :logseq.class/task)
           (update :block/refs (fn [refs]
@@ -150,26 +155,24 @@
     block))
 
 (defn- update-block-priority
-  [block db {:keys [log-fn]}]
+  [block {:keys [log-fn]}]
   (if-let [priority (:block/priority block)]
     (let [old-to-new {"A" :logseq.task/priority.high
                       "B" :logseq.task/priority.medium
                       "C" :logseq.task/priority.low}
-          priority-prop (:block/uuid (d/entity db :logseq.task/priority))
-          priority-ident (or (old-to-new priority)
+          priority-value (or (old-to-new priority)
                              (do
                                (log-fn :invalid-priority (str (pr-str priority) " is not a valid priority so setting it to low"))
-                               :logseq.task/priority.low))
-          priority-value (:block/uuid (d/entity db priority-ident))]
+                               :logseq.task/priority.low))]
       (-> block
-          (update :block/properties assoc priority-prop priority-value)
+          (assoc :logseq.task/priority priority-value)
           (update :block/content string/replace-first (re-pattern (str "\\[#" priority "\\]" "\\s*")) "")
           (update :block/refs (fn [refs]
                                 (into (remove #(= priority (:block/original-name %)) refs)
-                                      [:logseq.task/priority priority-ident])))
+                                      [:logseq.task/priority priority-value])))
           (update :block/path-refs (fn [refs]
                                      (into (remove #(= priority (:block/original-name %)) refs)
-                                           [:logseq.task/priority priority-ident])))
+                                           [:logseq.task/priority priority-value])))
           (dissoc :block/priority)))
     block))
 
@@ -265,8 +268,21 @@
     (when (and prev-type (not= prev-type prop-type))
       {:type {:from prev-type :to prop-type}})))
 
+(def built-in-property-name-to-idents
+  "Map of all built-in keyword property names to their idents. Using in-memory property
+  names because these are legacy names already in a user's file graph"
+  (->> db-property/built-in-properties
+       (map (fn [[k v]]
+              [(:name v) k]))
+       (into {})))
+
+(def built-in-property-names
+  "Set of all built-in property names as keywords. Using in-memory property
+  names because these are legacy names already in a user's file graph"
+  (->> built-in-property-name-to-idents keys set))
+
 (defn- update-built-in-property-values
-  [props db ignored-properties {:block/keys [content name]}]
+  [props {:keys [ignored-properties all-idents]} {:block/keys [content name]}]
   (->> props
        (keep (fn [[prop val]]
                (if (= :icon prop)
@@ -274,17 +290,17 @@
                             conj
                             {:property prop :value val :location (if name {:page name} {:block content})})
                      nil)
-                 [prop
+                 [(built-in-property-name-to-idents prop)
                   (case prop
                     :query-properties
                     (try
-                      (mapv #(if (#{:page :block :created-at :updated-at} %) % (get-pid db %))
+                      (mapv #(if (#{:page :block :created-at :updated-at} %) % (get-ident @all-idents %))
                             (edn/read-string val))
                       (catch :default e
                         (js/console.error "Translating query properties failed with:" e)
                         []))
                     :query-sort-by
-                    (if (#{:page :block :created-at :updated-at} val) val (get-pid db val))
+                    (if (#{:page :block :created-at :updated-at} val) val (get-ident @all-idents (keyword val)))
                     :filters
                     (try (edn/read-string val)
                          (catch :default e
@@ -354,18 +370,30 @@
       (throw (ex-info (str "No uuid found for page " (pr-str k))
                       {:page k}))))
 
-(def built-in-property-names
-  "Set of all built-in property names as keywords. Using in-memory property
-  names because these are legacy names already in a user's file graph"
-  (->> db-property/built-in-properties
-       vals
-       (map :name)
-       set))
+(defn- ->property-value-tx-m
+  "Given a new block and its properties, creates a map of properties which have values of property value tx.
+   Similar to sqlite.build/->property-value-tx-m"
+  [new-block properties get-schema-fn all-idents]
+  (->> properties
+       (keep (fn [[k v]]
+               (if-let [built-in-type (get-in db-property/built-in-properties [k :schema :type])]
+                 (when (and (db-property-type/value-ref-property-types built-in-type)
+                            ;; closed values are referenced by their :db/ident so no need to create values
+                            (not (get-in db-property/built-in-properties [k :closed-values])))
+                   (let [property-map {:db/ident k
+                                       :block/schema {:type built-in-type}}]
+                     [property-map v]))
+                 (when (db-property-type/value-ref-property-types (:type (get-schema-fn k)))
+                   (let [property-map {:db/ident (get-ident all-idents k)
+                                       :original-property-id k
+                                       :block/schema (get-schema-fn k)}]
+                     [property-map v])))))
+       (db-property-build/build-property-values-tx-m new-block)))
 
 (defn- build-properties-and-values
   "For given block properties, builds property values tx and returns a map with
   updated properties in :block-properties and any property values tx in :pvalues-tx"
-  [props db _page-names-to-uuids
+  [props _db _page-names-to-uuids
    {:block/keys [properties-text-values] :as block}
    {:keys [_whiteboard? import-state] :as options}]
   (let [;; FIXME: Whiteboard
@@ -391,17 +419,10 @@
       {}
       (let [props' (-> (update-built-in-property-values
                         (select-keys props built-in-property-names)
-                        db
-                        (:ignored-properties import-state)
+                        (select-keys import-state [:ignored-properties :all-idents])
                         (select-keys block [:block/name :block/content]))
                        (merge (update-user-property-values user-properties get-ident' properties-text-values import-state options)))
-            pvalue-tx-m (->> props'
-                             (map (fn [[k v]]
-                                    (let [property-map {:db/ident (get-ident @all-idents k)
-                                                        :original-property-id k
-                                                        :block/schema (get @property-schemas k)}]
-                                      [property-map v])))
-                             (db-property-build/build-property-values-tx-m block))
+            pvalue-tx-m (->property-value-tx-m block props' #(get @property-schemas %) @all-idents)
             block-properties (-> (merge props' (db-property-build/build-properties-with-ref-values pvalue-tx-m))
                                  (update-keys get-ident'))]
         {:block-properties block-properties
@@ -511,7 +532,7 @@
                         {:block/original-name new-class
                          :block/uuid (or (get-pid db new-class) (d/squuid))
                          :block/name (common-util/page-name-sanity-lc new-class)})))))
-          block*)]
+          (dissoc block* :block/properties))]
     {:block block' :properties-tx properties-tx}))
 
 (defn- handle-block-properties
@@ -556,37 +577,22 @@
                      (map #(add-uuid-to-page-map % page-names-to-uuids)))))
       block)))
 
-(defn- update-block-macros
-  [block db page-names-to-uuids]
-  (if (seq (:block/macros block))
-    (update block :block/macros
-            (fn [macros]
-              (mapv (fn [m]
-                      (-> m
-                          (update :block/properties
-                                  (fn [props]
-                                    (update-keys props #(cached-prop-name->uuid db page-names-to-uuids %))))
-                          (assoc :block/uuid (d/squuid))))
-                    macros)))
-    block))
-
 (defn- fix-pre-block-references
-  [{:block/keys [parent page] :as block} pre-blocks]
+  "Point pre-block children to parents since pre blocks don't exist in db graphs"
+  [{:block/keys [parent] :as block} pre-blocks page-names-to-uuids]
   (cond-> block
-    ;; Children blocks of pre-blocks get lifted up to the next level which can cause conflicts
-    ;; TODO: Detect sibling blocks to avoid parent-left conflicts
     (and (vector? parent) (contains? pre-blocks (second parent)))
-    (assoc :block/parent page)))
+    (assoc :block/parent [:block/uuid (get-page-uuid page-names-to-uuids (second (:block/page block)))])))
 
 (defn- fix-block-name-lookup-ref
   "Some graph-parser attributes return :block/name as a lookup ref. This fixes
   those to use uuids since block/name is not unique for db graphs"
-  [block db page-names-to-uuids]
+  [block page-names-to-uuids]
   (cond-> block
     (= :block/name (first (:block/page block)))
-    (assoc :block/page [:block/uuid (cached-prop-name->uuid db page-names-to-uuids (second (:block/page block)))])
+    (assoc :block/page [:block/uuid (get-page-uuid page-names-to-uuids (second (:block/page block)))])
     (:block/name (:block/parent block))
-    (assoc :block/parent {:block/uuid (cached-prop-name->uuid db page-names-to-uuids (:block/name (:block/parent block)))})))
+    (assoc :block/parent {:block/uuid (get-page-uuid page-names-to-uuids (:block/name (:block/parent block)))})))
 
 (defn- build-block-tx
   [db block* pre-blocks page-names-to-uuids {:keys [tag-classes] :as options}]
@@ -596,13 +602,12 @@
         (handle-block-properties block* db page-names-to-uuids (:block/refs block*) options)
         {block-after-built-in-props :block deadline-properties-tx :properties-tx} (update-block-deadline block db options)
         block' (-> block-after-built-in-props
-                   (fix-pre-block-references pre-blocks)
-                   (fix-block-name-lookup-ref db page-names-to-uuids)
-                   (update-block-macros db page-names-to-uuids)
+                   (fix-pre-block-references pre-blocks page-names-to-uuids)
+                   (fix-block-name-lookup-ref page-names-to-uuids)
                    (update-block-refs page-names-to-uuids options)
-                   (update-block-tags tag-classes page-names-to-uuids)
-                   (update-block-marker db options)
-                   (update-block-priority db options)
+                   (update-block-tags db tag-classes page-names-to-uuids)
+                   (update-block-marker options)
+                   (update-block-priority options)
                    add-missing-timestamps
                    ;; ((fn [x] (prn :block-out x) x))
                    ;; TODO: org-mode content needs to be handled
@@ -611,7 +616,7 @@
     (concat properties-tx deadline-properties-tx [block'])))
 
 (defn- build-new-page
-  [m tag-classes page-names-to-uuids page-tags-uuid]
+  [m db tag-classes page-names-to-uuids]
   (-> m
       ;; Fix pages missing :block/original-name. Shouldn't happen
       ((fn [m']
@@ -622,13 +627,13 @@
       ;; TODO: org-mode content needs to be handled
       (assoc :block/format :markdown)
       (dissoc :block/whiteboard?)
-      (update-page-tags tag-classes page-names-to-uuids page-tags-uuid)))
+      (update-page-tags db tag-classes page-names-to-uuids)))
 
 (defn- build-pages-tx
   "Given all the pages and blocks parsed from a file, return a map containing
   all non-whiteboard pages to be transacted, pages' properties and additional
   data for subsequent steps"
-  [conn pages blocks {:keys [page-tags-uuid tag-classes property-classes property-parent-classes notify-user]
+  [conn pages blocks {:keys [tag-classes property-classes property-parent-classes notify-user]
                       :as options}]
   (let [all-pages (->> (extract/with-ref-pages pages blocks)
                        ;; remove unused property pages unless the page has content
@@ -658,8 +663,8 @@
                              (when (seq block-changes)
                                (cond-> (merge block-changes {:block/uuid page-uuid})
                                  (:block/tags m)
-                                 (update-page-tags tag-classes page-names-to-uuids page-tags-uuid))))
-                           (build-new-page m tag-classes page-names-to-uuids page-tags-uuid)))
+                                 (update-page-tags @conn tag-classes page-names-to-uuids))))
+                           (build-new-page m @conn tag-classes page-names-to-uuids)))
                        (map :block all-pages-m))]
     {:pages-tx pages-tx
      :page-properties-tx (mapcat :properties-tx all-pages-m)
@@ -747,7 +752,7 @@
   pages that are now properties"
   [pages-tx old-properties existing-pages import-state]
   (let [new-properties (set/difference (set (keys @(:property-schemas import-state))) (set old-properties))
-        _ (prn :new-properties new-properties)
+        _ (when (seq new-properties) (prn :new-properties new-properties))
         [properties-tx pages-tx'] ((juxt filter remove)
                                    #(contains? new-properties (keyword (:block/name %))) pages-tx)
         property-pages-tx (map (fn [{:block/keys [original-name uuid]}]
@@ -795,7 +800,6 @@
 * :extract-options - Options map to pass to extract/extract
 * :user-options - User provided options maps that alter how a file is converted to db graph. Current options
    are :tag-classes (set) and :property-classes (set).
-* :page-tags-uuid - uuid of pageTags property
 * :import-state - useful import state to maintain across files e.g. property schemas or ignored properties
 * :macros - map of macros for use with macro expansion
 * :notify-user - Displays warnings to user without failing the import. Fn receives a map with :msg
@@ -1024,14 +1028,13 @@
 
 (defn build-doc-options
   "Builds options for use with export-doc-files"
-  [conn config options]
+  [config options]
   (-> {:extract-options {:date-formatter (common-config/get-date-formatter config)
                          :user-config config
                          :filename-format (or (:file/name-format config) :legacy)
                          :verbose (:verbose options)}
        :user-config config
        :user-options (select-keys options [:tag-classes :property-classes :property-parent-classes])
-       :page-tags-uuid (:block/uuid (d/entity @conn :logseq.property/page-tags))
        :import-state (new-import-state)
        :macros (or (:macros options) (:macros config))}
       (merge (select-keys options [:set-ui-state :export-file :notify-user]))))
@@ -1066,7 +1069,7 @@
                          (remove logseq-file?)
                          (filter #(contains? #{"md" "org" "markdown" "edn"} (path/file-ext (:path %)))))
           asset-files (filter #(string/starts-with? (get % rpath-key) "assets/") files)
-          doc-options (build-doc-options conn config options)]
+          doc-options (build-doc-options config options)]
       (log-fn "Importing" (count files) "files ...")
       ;; These export* fns are all the major export/import steps
       (p/do!

+ 114 - 29
deps/graph-parser/test/logseq/graph_parser/exporter_test.cljs

@@ -12,15 +12,27 @@
             [logseq.db.sqlite.create-graph :as sqlite-create-graph]
             [logseq.graph-parser.exporter :as gp-exporter]
             [logseq.db.frontend.malli-schema :as db-malli-schema]
-            [logseq.db.frontend.property :as db-property]))
+            [logseq.db.frontend.property :as db-property]
+            [logseq.db.frontend.property.type :as db-property-type]))
+
+;; Helpers
+;; =======
+;; some have been copied from db-import script
 
 (defn- find-block-by-content [db content]
-  (->> content
-       (d/q '[:find [(pull ?b [*]) ...]
-              :in $ ?content
-              :where [?b :block/content ?content]]
-            db)
-       first))
+  (if (instance? js/RegExp content)
+    (->> content
+         (d/q '[:find [(pull ?b [*]) ...]
+                :in $ ?pattern
+                :where [?b :block/content ?content] [(re-find ?pattern ?content)]]
+              db)
+         first)
+    (->> content
+         (d/q '[:find [(pull ?b [*]) ...]
+                :in $ ?content
+                :where [?b :block/content ?content]]
+              db)
+         first)))
 
 (defn- find-page-by-name [db name]
   (->> name
@@ -88,13 +100,37 @@
                      (dissoc :assets))]
     (gp-exporter/export-file-graph conn conn config-file *files options')))
 
+(defn- import-files-to-db
+  "Import specific doc files for dev purposes"
+  [files conn options]
+  (let [doc-options (gp-exporter/build-doc-options {:macros {}} (merge options default-export-options))
+        files' (mapv #(hash-map :path %) files)]
+    (gp-exporter/export-doc-files conn files' <read-file doc-options)))
+
+(defn- readable-properties
+  [db query-ent]
+  (->> (db-property/properties query-ent)
+       (map (fn [[k v]]
+              [k
+               (if-let [built-in-type (get-in db-property/built-in-properties [k :schema :type])]
+                 (if (= :block/tags k)
+                   (mapv #(:db/ident (d/entity db (:db/id %))) v)
+                   (if (db-property-type/ref-property-types built-in-type)
+                     (db-property/ref->property-value-contents db v)
+                     v))
+                 (db-property/ref->property-value-contents db v))]))
+       (into {})))
+
+;; Tests
+;; =====
+
 (deftest-async export-basic-graph
   ;; This graph will contain basic examples of different features to import
   (p/let [file-graph-dir "test/resources/exporter-test-graph"
           conn (d/create-conn db-schema/schema-for-db-based-graph)
           _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
           assets (atom [])
-          _ (import-file-graph-to-db file-graph-dir conn {:assets assets})]
+          {:keys [import-state]} (import-file-graph-to-db file-graph-dir conn {:assets assets})]
 
     (is (nil? (:errors (db-validate/validate-db! @conn)))
         "Created graph has no validation errors")
@@ -106,10 +142,10 @@
              (ffirst (d/q '[:find ?content :where [?b :file/path "logseq/custom.js"] [?b :file/content ?content]] @conn)))))
 
     (testing "graph wide counts"
-      ;; Includes 2 journals from logseq.task/deadline
-      (is (= 6 (count (d/q '[:find ?b :where [?b :block/type "journal"]] @conn))))
-      ;; Count includes Contents
-      (is (= 3
+      ;; Includes 2 journals as property values for :logseq.task/deadline
+      (is (= 8 (count (d/q '[:find ?b :where [?b :block/type "journal"]] @conn))))
+      ;; Count includes Contents and page references
+      (is (= 7
              (count (d/q '[:find (pull ?b [*]) :where [?b :block/original-name ?name] (not [?b :block/type])] @conn))))
       (is (= 1 (count @assets))))
 
@@ -129,8 +165,7 @@
               :user.property/prop-num 5
               :user.property/prop-string "woot"}
              (update-vals (db-property/properties (find-block-by-content @conn "b1"))
-                          (fn [ref]
-                            (db-property/ref->property-value-content @conn ref))))
+                          #(db-property/ref->property-value-content @conn %)))
           "Basic block has correct properties")
       (is (= #{"prop-num" "prop-string" "prop-bool"}
              (->> (d/entity @conn (:db/id (find-block-by-content @conn "b1")))
@@ -140,27 +175,77 @@
           "Block with properties has correct refs")
 
       (is (= {:user.property/prop-num2 10}
-             (update-vals (db-property/properties (find-page-by-name @conn "new page"))
-                          (fn [ref]
-                            (db-property/ref->property-value-content @conn ref))))
+             (readable-properties @conn (find-page-by-name @conn "new page")))
           "New page has correct properties")
       (is (= {:user.property/prop-bool true
               :user.property/prop-num 5
               :user.property/prop-string "yeehaw"}
-             (update-vals (db-property/properties (find-page-by-name @conn "some page"))
-                          (fn [ref]
-                            (db-property/ref->property-value-content @conn ref))))
+             (readable-properties @conn (find-page-by-name @conn "some page")))
           "Existing page has correct properties"))
 
     (testing "built-in properties"
-      (is (= {:logseq.task/deadline "Nov 25th, 2022"}
-             (update-vals (db-property/properties (find-block-by-content @conn "only scheduled"))
-                          (fn [ref]
-                            (db-property/ref->property-value-content @conn ref))))
-          "deadline block has correct journal")
+      (is (= 2
+             (count (filter #(= :icon (:property %)) @(:ignored-properties import-state))))
+          "icon properties are visibly ignored in order to not fail import")
+
+      (is (= {:logseq.task/deadline "Nov 26th, 2022"}
+             (readable-properties @conn (find-block-by-content @conn "only deadline")))
+          "deadline block has correct journal as property value")
 
       (is (= {:logseq.task/deadline "Nov 25th, 2022"}
-             (update-vals (db-property/properties (find-block-by-content @conn "only scheduled"))
-                          (fn [ref]
-                            (db-property/ref->property-value-content @conn ref))))
-          "scheduled block converted to deadline"))))
+             (readable-properties @conn (find-block-by-content @conn "only scheduled")))
+          "scheduled block converted to correct deadline")
+
+      (is (= {:logseq.task/priority "High"}
+             (readable-properties @conn (find-block-by-content @conn "high priority")))
+          "priority block has correct property")
+
+      (is (= {:logseq.task/status "Doing" :logseq.task/priority "Medium" :block/tags [:logseq.class/task]}
+             (readable-properties @conn (find-block-by-content @conn "status test")))
+          "status block has correct task properties and class")
+
+      (is (= #{:logseq.task/status :block/tags}
+             (set (keys (readable-properties @conn (find-block-by-content @conn "old todo block")))))
+          "old task properties are ignored")
+
+      (is (= {:logseq.property/query-sort-by :user.property/prop-num
+              :logseq.property/query-properties [:block :page :user.property/prop-string :user.property/prop-num]
+              :logseq.property/query-table true}
+             (readable-properties @conn (find-block-by-content @conn "{{query (property :prop-string)}}")))
+          "query block has correct query properties"))
+
+    (testing "tags without tag options"
+      (let [block (find-block-by-content @conn #"Inception")
+            tag-page (find-page-by-name @conn "Movie")
+            tagged-page (find-page-by-name @conn "Interstellar")]
+        (is (string/starts-with? (str (:block/content block)) "Inception [[")
+            "tagged block tag converts tag to page ref")
+        (is (= [(:db/id tag-page)] (map :db/id (:block/refs block)))
+            "tagged block has correct refs")
+        (is (and tag-page (not (:block/type tag-page)))
+            "tag page is not a class")
+
+        (is (= {:logseq.property/page-tags #{"Movie"}}
+               (readable-properties @conn tagged-page))
+            "tagged page has tags imported as page-tags property by default")))))
+
+(deftest-async export-file-with-tag-classes-option
+  (p/let [file-graph-dir "test/resources/exporter-test-graph"
+          files (mapv #(node-path/join file-graph-dir %) ["journals/2024_02_07.md" "pages/Interstellar.md"])
+          conn (d/create-conn db-schema/schema-for-db-based-graph)
+          _ (d/transact! conn (sqlite-create-graph/build-db-initial-data "{}"))
+          _ (import-files-to-db files conn {:tag-classes ["movie"]})]
+    (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")]
+      (is (= (:block/content block) "Inception")
+          "tagged block with configured tag strips tag from content")
+
+      (is (= ["class"] (:block/type tag-page))
+          "configured tag page in :tag-classes is a class")
+      (is (and another-tag-page (not (:block/type another-tag-page)))
+          "unconfigured tag page is not a class")
+
+      (is (= {:block/tags [:user.class/Movie]}
+             (readable-properties @conn (find-page-by-name @conn "Interstellar")))
+          "tagged page has configured tag imported as a class"))))

+ 1 - 0
deps/graph-parser/test/resources/exporter-test-graph/.gitignore

@@ -0,0 +1 @@
+/logseq/bak

+ 2 - 0
deps/graph-parser/test/resources/exporter-test-graph/journals/2024_02_07.md

@@ -0,0 +1,2 @@
+- Inception #Movie
+- TODO do X #p0

+ 9 - 0
deps/graph-parser/test/resources/exporter-test-graph/journals/2024_02_14.md

@@ -0,0 +1,9 @@
+- TODO old todo block
+  todo:: 1612237041309
+  done:: 1612237041309
+  now:: 1612237041309
+  later:: 1612237041309
+- {{query (property :prop-string)}}
+  query-table:: true
+  query-properties:: [:block :page :prop-string :prop-num]
+  query-sort-by:: prop-num

+ 6 - 1
deps/graph-parser/test/resources/exporter-test-graph/journals/2024_04_01.md

@@ -1,4 +1,9 @@
 - only deadline
   DEADLINE: <2022-11-26 Sat>
 - only scheduled
-  SCHEDULED: <2022-11-25 Fri>
+  SCHEDULED: <2022-11-25 Fri>
+- [#A] high priority
+- DOING [#B] status test
+  :LOGBOOK:
+  CLOCK: [2024-04-01 Mon 10:39:40]
+  :END:

+ 3 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/Interstellar.md

@@ -0,0 +1,3 @@
+tags:: Movie
+
+-

+ 1 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/contents.md

@@ -0,0 +1 @@
+-

+ 4 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/icon page.md

@@ -0,0 +1,4 @@
+icon:: 😆
+
+- has some content
+  icon:: 😆

+ 3 - 0
deps/graph-parser/test/resources/exporter-test-graph/pages/new page.md

@@ -1 +1,4 @@
 prop-num2:: 10
+
+    - test if pre-block child causes issue
+        - grandchild test

+ 1 - 1
deps/outliner/src/logseq/outliner/property.cljs

@@ -345,7 +345,7 @@
                  current-parent
                  (contains? (:block/type parent) "class")
                  (not (contains? @*classes (:db/id parent))))
-            (swap! *classes conj current-parent)
+            (swap! *classes conj (:db/id current-parent))
             (recur (:class/parent current-parent))))))
     @*classes))
 

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

@@ -188,9 +188,9 @@
       [:div.ml-1
        (when selected?
          (shui/button {:variant :destructive
-                       :class "text-muted-foreground"
+                       :class "text-red-500"
                        :size :sm
-                       :on-click #(state/set-modal!
+                       :on-click #(shui/dialog-open!
                                    (component-page/batch-delete-dialog selected-rows false (fn [] (set-data! (get-all-pages)))))}
                       (ui/icon "trash-x")))]
       [:div.flex.items-center.gap-2

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

@@ -3013,7 +3013,7 @@
        (block-positioned-properties config block :block-below))
 
      (when (and db-based? (not collapsed?) (not table?))
-       [:div {:style {:padding-left 29}}
+       [:div {:style {:padding-left 45}}
         (db-properties-cp config block edit-input-id {:in-block-container? true})])
 
      (when-not (or (:hide-children? config) in-whiteboard? table?)

+ 3 - 1
src/main/frontend/components/file_based/hierarchy.cljs

@@ -16,7 +16,9 @@
   [page]
   (when-let [page (or (text/get-nested-page-name page) page)]
     (let [repo (state/get-current-repo)
-          aliases (db/get-page-alias-names repo page)
+          page-entity (db/get-page page)
+          aliases (when-let [page-id (:db/id page-entity)]
+                    (db/get-page-alias-names repo page-id))
           all-page-names (conj aliases page)]
       (when-let [page (or (first (filter text/namespace-page? all-page-names))
                           (when (:block/_namespace (db/entity [:block/name (util/page-name-sanity-lc page)]))

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

@@ -823,21 +823,21 @@
 (defn make-onboarding-panel
   [type]
 
-  (fn [close-fn]
+  (fn [{:keys [close]}]
 
     (case type
       :welcome
-      (onboarding-welcome-logseq-sync close-fn)
+      (onboarding-welcome-logseq-sync close)
 
       :unavailable
-      (onboarding-unavailable-file-sync close-fn)
+      (onboarding-unavailable-file-sync close)
 
       :congrats
-      (onboarding-congrats-successful-sync close-fn)
+      (onboarding-congrats-successful-sync close)
 
       [:p
        [:h1.text-xl.font-bold "Not handled!"]
-       [:a.button {:on-click close-fn} "Got it!"]])))
+       [:a.button {:on-click close} "Got it!"]])))
 
 (defn maybe-onboarding-show
   [type]

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

@@ -77,7 +77,9 @@
           (shui/tabler-icon "user-plus")
           (shui/tabler-icon "user-plus"))]
        (when (seq online-users)
-         (for [{:keys [user-email user-name user-uuid]} online-users
+         (for [{user-email :user/email
+                user-name :user/name
+                user-uuid :user/uuid} online-users
                :let [color (shui-util/uuid-color user-uuid)]]
            (when user-name
              (shui/avatar

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

@@ -433,7 +433,7 @@
        nil))))
 
 (defn- get-filter-with-changed-operator
-  [property operator value]
+  [_property operator value]
   (case operator
     (:is :is-not)
     (when (set? value) value)
@@ -478,7 +478,7 @@
          (operator->text operator)))))))
 
 (rum/defc between < rum/static
-  [property [start end] filters set-filters! idx]
+  [_property [start end] filters set-filters! idx]
   [:<>
    (shui/input
     {:auto-focus true

+ 55 - 47
src/main/frontend/components/page.cljs

@@ -601,14 +601,19 @@
   {:init (fn [state]
            (let [page-name (:page-name (first (:rum/args state)))
                  page-name' (get-sanity-page-name state page-name)
+                 page-uuid? (util/uuid-string? page-name')
                  *loading? (atom true)]
-             (p/do!
-              (db-async/<get-block (state/get-current-repo) page-name')
-              (reset! *loading? false)
-              (route-handler/update-page-title-and-label! (state/get-route-match)))
+             (p/let
+               [page-block (db-async/<get-block (state/get-current-repo) page-name')]
+               (reset! *loading? false)
+               (if (not page-block)
+                 (page-handler/<create! page-name' {:redirect? true})
+                 (if-let [page-uuid (and (not page-uuid?) (:block/uuid page-block))]
+                   (route-handler/redirect-to-page! (str page-uuid) {:push false})
+                   (route-handler/update-page-title-and-label! (state/get-route-match)))))
              (assoc state
-                    ::page-name page-name'
-                    ::loading? *loading?)))}
+               ::page-name page-name'
+               ::loading? *loading?)))}
   [state option]
   (page-inner (assoc option :*loading? (::loading? state))))
 
@@ -617,12 +622,12 @@
   (rum/with-key
     (page-aux option)
     (or (:page-name option)
-        (get-page-name state))))
+      (get-page-name state))))
 
 (rum/defc contents-page < rum/reactive
-  {:init (fn [state]
-           (db-async/<get-block (state/get-current-repo) "contents")
-           state)}
+                          {:init (fn [state]
+                                   (db-async/<get-block (state/get-current-repo) "contents")
+                                   state)}
   [page]
   (when-let [repo (state/get-current-repo)]
     (when-not (state/sub-async-query-loading "contents")
@@ -1014,7 +1019,7 @@
 
 (defn batch-delete-dialog
   [pages orphaned-pages? refresh-fn]
-  (fn [close-fn]
+  (fn [{:keys [close]}]
     [:div
      [:div.sm:flex.items-center
       [:div.mx-auto.flex-shrink-0.flex.items-center.justify-center.h-12.w-12.rounded-full.bg-error.sm:mx-0.sm:h-10.sm:w-10
@@ -1026,44 +1031,47 @@
           (t :remove-orphaned-pages)
           (t :page/delete-confirmation))]]]
 
-     [:table.table-auto.cp__all_pages_table.mt-4
-      [:thead
-       [:tr.opacity-70
-        [:th [:span "#"]]
-        [:th [:span (t :block/name)]]
-        [:th [:span (t :page/backlinks)]]
-        (when-not orphaned-pages? [:th [:span (t :page/created-at)]])
-        (when-not orphaned-pages? [:th [:span (t :page/updated-at)]])]]
-
-      [:tbody
-       (for [[n {:block/keys [name created-at updated-at backlinks] :as page}] (medley/indexed pages)]
-         [:tr {:key name}
-          [:td.n.w-12 [:span.opacity-70 (str (inc n) ".")]]
-          [:td.name [:a {:href     (rfe/href :page {:name (:block/uuid page)})}
-                     (component-block/page-cp {} page)]]
-          [:td.backlinks [:span (or backlinks "0")]]
-          (when-not orphaned-pages? [:td.created-at [:span (if created-at (date/int->local-time-2 created-at) "Unknown")]])
-          (when-not orphaned-pages? [:td.updated-at [:span (if updated-at (date/int->local-time-2 updated-at) "Unknown")]])])]]
+     [:div.cp__all_pages_table-wrap
+      [:table.w-full.cp__all_pages_table.mt-4
+       [:thead
+        [:tr.opacity-70
+         [:th [:span "#"]]
+         [:th [:span (t :block/name)]]
+         [:th [:span (t :page/backlinks)]]
+         (when-not orphaned-pages? [:th [:span (t :page/created-at)]])
+         (when-not orphaned-pages? [:th [:span (t :page/updated-at)]])]]
+
+       [:tbody
+        (for [[n {:block/keys [name created-at updated-at backlinks] :as page}] (medley/indexed pages)]
+          [:tr {:key name}
+           [:td.n.w-12 [:span.opacity-70 (str (inc n) ".")]]
+           [:td.name [:a {:href (rfe/href :page {:name (:block/uuid page)})}
+                      (component-block/page-cp {} page)]]
+           [:td.backlinks [:span (or backlinks "0")]]
+           (when-not orphaned-pages? [:td.created-at [:span (if created-at (date/int->local-time-2 created-at) "Unknown")]])
+           (when-not orphaned-pages? [:td.updated-at [:span (if updated-at (date/int->local-time-2 updated-at) "Unknown")]])])]]]
+
+     [:p.px-1.opacity-50 [:small (str "Total: " (count pages))]]
 
      [:div.pt-6.flex.justify-end.gap-4
       (ui/button
-       (t :cancel)
-       :theme :gray
-       :on-click close-fn)
+        (t :cancel)
+        :variant :outline
+        :on-click close)
 
       (ui/button
-       (t :yes)
-       :on-click (fn []
-                   (close-fn)
-                   (let [failed-pages (atom [])]
-                     (p/let [_ (p/all (map (fn [page]
-                                             (page-handler/<delete! (:block/uuid page) nil
-                                                                    {:error-handler
-                                                                     (fn []
-                                                                       (swap! failed-pages conj (:block/name page)))}))
-                                           pages))]
-                       (if (seq @failed-pages)
-                         (notification/show! (t :all-pages/failed-to-delete-pages (string/join ", " (map pr-str @failed-pages)))
-                                             :warning false)
-                         (notification/show! (t :tips/all-done) :success))))
-                   (js/setTimeout #(refresh-fn) 200)))]]))
+        (t :yes)
+        :on-click (fn []
+                    (close)
+                    (let [failed-pages (atom [])]
+                      (p/let [_ (p/all (map (fn [page]
+                                              (page-handler/<delete! (:block/uuid page) nil
+                                                {:error-handler
+                                                 (fn []
+                                                   (swap! failed-pages conj (:block/name page)))}))
+                                         pages))]
+                        (if (seq @failed-pages)
+                          (notification/show! (t :all-pages/failed-to-delete-pages (string/join ", " (map pr-str @failed-pages)))
+                            :warning false)
+                          (notification/show! (t :tips/all-done) :success))))
+                    (js/setTimeout #(refresh-fn) 200)))]]))

+ 33 - 61
src/main/frontend/components/property.cljs

@@ -819,8 +819,7 @@
                                         (ldb/page? block))
                             (outliner-property/property-with-other-position? ent)))))))
                   properties))
-        {:keys [classes all-classes classes-properties]} (outliner-property/get-block-classes-properties (db/get-db) (:db/id block))
-        one-class? (= 1 (count classes))
+        {:keys [all-classes classes-properties]} (outliner-property/get-block-classes-properties (db/get-db) (:db/id block))
         classes-properties-set (set classes-properties)
         block-own-properties (->> properties
                                   (remove (fn [[id _]] (classes-properties-set id)))
@@ -850,31 +849,24 @@
                           (comp hide-with-property-id first))
         {_block-hidden-properties true
          block-own-properties' false} (group-by property-hide-f block-own-properties)
-        {_class-hidden-properties true
-         class-own-properties false} (group-by property-hide-f
-                                               (map (fn [id] [id (get block-properties id)]) classes-properties))
-        own-properties (->>
-                        (if one-class?
-                          (->> (concat block-own-properties' class-own-properties)
-                               remove-built-in-or-other-position-properties)
-                          block-own-properties'))
-        class->properties (loop [classes all-classes
-                                 properties #{}
-                                 result []]
-                            (if-let [class (first classes)]
-                              (let [cur-properties (->> (db-property/get-class-ordered-properties class)
-                                                        (map :db/ident)
-                                                        (remove properties)
-                                                        (remove hide-with-property-id)
-                                                        remove-built-in-or-other-position-properties)]
-                                (recur (rest classes)
-                                       (set/union properties (set cur-properties))
-                                       (if (seq cur-properties)
-                                         (conj result [class cur-properties])
-                                         result)))
-                              result))]
-    (when-not (and (empty? block-own-properties')
-                   (empty? class->properties)
+        class-properties (loop [classes all-classes
+                                properties (set (map first block-own-properties'))
+                                result []]
+                           (if-let [class (first classes)]
+                             (let [cur-properties (->> (db-property/get-class-ordered-properties class)
+                                                       (map :db/ident)
+                                                       (remove properties)
+                                                       (remove hide-with-property-id)
+                                                       remove-built-in-or-other-position-properties)]
+                               (recur (rest classes)
+                                      (set/union properties (set cur-properties))
+                                      (if (seq cur-properties)
+                                        (into result cur-properties)
+                                        result)))
+                             result))
+        full-properties (->> (concat block-own-properties' (map (fn [p] [p nil]) class-properties))
+                             remove-built-in-or-other-position-properties)]
+    (when-not (and (empty? full-properties)
                    (not (:page-configure? opts)))
       [:div.ls-properties-area
        (cond-> {:id id}
@@ -888,37 +880,17 @@
                                        (state/set-selection-blocks! [block])
                                        (some-> js/document.activeElement (.blur)))
                                      (d/remove-class! target "ls-popup-closed")))))
-       (let [own-properties' (cond
-                               (and page? page-configure?)
-                               (concat [[:block/tags (:block/tags block)]
-                                        [:logseq.property/icon (:logseq.property/icon block)]]
-                                       (remove (fn [[k _v]] (contains? #{:block/tags :logseq.property/icon} k)) own-properties))
-
-                               page?
-                               (remove (fn [[k _v]] (contains? #{:logseq.property/icon} k)) own-properties)
-
-                               :else
-                               own-properties)]
-         (properties-section block (if class-schema? properties own-properties') opts))
-
-       (rum/with-key (new-property block opts) (str id "-add-property"))
-
-       (when (and (seq class->properties) (not one-class?))
-         (let [class-properties-col (keep
-                                     (fn [[class class-properties]]
-                                       (let [properties (->> class-properties
-                                                             (map (fn [id] [id (get block-properties id)])))]
-                                         (when (seq properties)
-                                           [class properties])))
-                                     class->properties)]
-           (when (seq class-properties-col)
-             (let [page-cp (:page-cp opts)]
-               [:div.parent-properties.flex.flex-1.flex-col.gap-1
-                (for [[class id-properties] class-properties-col]
-                  (when (seq id-properties)
-                    [:div
-                     (when page-cp
-                       [:span.text-sm.opacity-30.hover:opacity-100
-                        {:class (when (:in-block-container? opts) "ml-5")}
-                        (page-cp {} class)])
-                     (properties-section block id-properties opts)]))]))))])))
+       (let [properties' (cond
+                           (and page? page-configure?)
+                           (concat [[:block/tags (:block/tags block)]
+                                    [:logseq.property/icon (:logseq.property/icon block)]]
+                                   (remove (fn [[k _v]] (contains? #{:block/tags :logseq.property/icon} k)) full-properties))
+
+                           page?
+                           (remove (fn [[k _v]] (contains? #{:logseq.property/icon} k)) full-properties)
+
+                           :else
+                           full-properties)]
+         (properties-section block (if class-schema? properties properties') opts))
+
+       (rum/with-key (new-property block opts) (str id "-add-property"))])))

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

@@ -25,8 +25,7 @@
             [datascript.impl.entity :as de]
             [frontend.handler.property.util :as pu]
             [logseq.db.frontend.property.type :as db-property-type]
-            [dommy.core :as d]
-            [frontend.db-mixins :as db-mixins]))
+            [dommy.core :as d]))
 
 (rum/defc property-empty-btn-value
   [& {:as opts}]

+ 47 - 38
src/main/frontend/components/server.cljs

@@ -144,41 +144,50 @@
       [error])
 
     [:div.cp__server-indicator
-     ;; settings menus
-     (ui/dropdown-with-links
-       (fn [{:keys [toggle-fn]}]
-         [:button.button.icon
-          {:on-click #(toggle-fn)}
-          (ui/icon (if running? "api" "api-off") {:size 22})])
-
-       ;; items
-       (->> [{:hr true}
-
-             (cond
-               running?
-               {:title   "Stop server"
-                :options {:on-click #(ipc/ipc :server/do :stop)}
-                :icon    [:span.text-red-500.flex.items-center (ui/icon "player-stop")]}
-
-               :else
-               {:title   "Start server"
-                :options {:on-click #(ipc/ipc :server/do :restart)}
-                :icon    [:span.text-green-500.flex.items-center (ui/icon "player-play")]})
-
-             {:title   "Authorization tokens"
-              :options {:on-click #(shui/dialog-open!
-                                     (fn []
-                                       (panel-of-tokens shui/dialog-close!)))}
-              :icon (ui/icon "key")}
-
-             {:title "Server configurations"
-              :options {:on-click #(shui/dialog-open!
-                                     (fn []
-                                       (panel-of-configs shui/dialog-close!)))}
-              :icon (ui/icon "server-cog")}])
-       {:links-header
-        [:div.links-header.flex.justify-center.py-2
-         [:span.ml-1.text-sm
-          (if-not running?
-            (string/upper-case (or (:status server-state) "stopped"))
-            [:a.hover:underline {:href href} href])]]})]))
+     [:button.button.icon
+      {:on-click (fn [^js e]
+                   (shui/popup-show!
+                     (.-target e)
+                     (fn [{:keys [_close]}]
+                       (let [items [{:hr? true}
+
+                                    (cond
+                                      running?
+                                      {:title "Stop server"
+                                       :options {:on-click #(ipc/ipc :server/do :stop)}
+                                       :icon [:span.text-red-500.flex.items-center (ui/icon "player-stop")]}
+
+                                      :else
+                                      {:title "Start server"
+                                       :options {:on-click #(ipc/ipc :server/do :restart)}
+                                       :icon [:span.text-green-500.flex.items-center (ui/icon "player-play")]})
+
+                                    {:title "Authorization tokens"
+                                     :options {:on-click #(shui/dialog-open!
+                                                            (fn []
+                                                              (panel-of-tokens shui/dialog-close!)))}
+                                     :icon (ui/icon "key")}
+
+                                    {:title "Server configurations"
+                                     :options {:on-click #(shui/dialog-open!
+                                                            (fn []
+                                                              (panel-of-configs shui/dialog-close!)))}
+                                     :icon (ui/icon "server-cog")}]]
+
+                         (cons
+                           [:div.links-header.flex.justify-center.py-2
+                            [:span.ml-1.text-sm.opacity-70
+                             (if-not running?
+                               (string/upper-case (or (:status server-state) "stopped"))
+                               [:a.hover:underline {:href href} href])]]
+                           (for [{:keys [hr? title options icon]} items]
+                             (cond
+                               hr?
+                               (shui/dropdown-menu-separator)
+
+                               :else
+                               (shui/dropdown-menu-item options
+                                 [:span.flex.items-center icon [:span.pl-1 title]]))))))
+                     {:as-dropdown? true
+                      :content-props {:onClick #(shui/popup-hide!)}}))}
+      (ui/icon (if running? "api" "api-off") {:size 22})]]))

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

@@ -1140,7 +1140,9 @@
      [:div.flex.flex-col.gap-2.mt-4
       [:h2.opacity-50.font-medium "Members:"]
       [:div.users.flex.flex-col.gap-1
-       (for [{:keys [user-name user-email graph<->user-user-type]} users]
+       (for [{user-name :user/name
+              user-email :user/email
+              graph<->user-user-type :graph<->user/user-type} users]
          [:div.flex.flex-row.items-center.gap-2 {:key (str "user-" user-name)}
           [:div user-name]
           (when user-email [:div.opacity-50.text-sm user-email])

+ 9 - 8
src/main/frontend/components/whiteboard.cljs

@@ -160,14 +160,15 @@
            {:icon "trash"
             :on-click
             (fn []
-              (state/set-modal! (page/batch-delete-dialog
-                                 (map (fn [id]
-                                        (some (fn [w] (when (= (:db/id w) id) w)) whiteboards))
-                                      checked-page-ids)
-                                 false
-                                 (fn []
-                                   (set-checked-page-ids #{})
-                                   (route-handler/redirect-to-whiteboard-dashboard!)))))}))]
+              (shui/dialog-open!
+                (page/batch-delete-dialog
+                  (map (fn [id]
+                         (some (fn [w] (when (= (:db/id w) id) w)) whiteboards))
+                    checked-page-ids)
+                  false
+                  (fn []
+                    (set-checked-page-ids #{})
+                    (route-handler/redirect-to-whiteboard-dashboard!)))))}))]
        [:div
         {:ref ref}
         [:div.gap-8.grid.grid-rows-auto

+ 6 - 2
src/main/frontend/db/utils.cljs

@@ -26,11 +26,15 @@
    (entity (state/get-current-repo) eid))
   ([repo-or-db eid]
    (when eid
+     (assert (or (number? eid)
+                 (sequential? eid)
+                 (keyword? eid))
+             (str "Invalid entity eid: " (pr-str eid)))
      (when-let [db (if (string? repo-or-db)
-                   ;; repo
+                     ;; repo
                      (let [repo (or repo-or-db (state/get-current-repo))]
                        (conn/get-db repo))
-                   ;; db
+                     ;; db
                      repo-or-db)]
        (d/entity db eid)))))
 

+ 1 - 0
src/main/frontend/handler/db_based/recent.cljs

@@ -6,6 +6,7 @@
 
 (defn add-page-to-recent!
   [db-id click-from-recent?]
+  (assert db-id (number? db-id))
   (when-not (:db/restoring? @state/state)
     (when-let [page (db/entity db-id)]
       (when-not (ldb/hidden-page? page)

+ 6 - 5
src/main/frontend/handler/events.cljs

@@ -752,11 +752,12 @@
 
 (defmethod handle :file-sync/onboarding-tip [[_ type opts]]
   (let [type (keyword type)]
-    (shui/dialog-open!
-      (file-sync/make-onboarding-panel type)
-      (merge {:close-btn?      false
-              :center?         true
-              :close-backdrop? (not= type :welcome)} opts))))
+    (when-not (config/db-based-graph? (state/get-current-repo))
+      (shui/dialog-open!
+        (file-sync/make-onboarding-panel type)
+        (merge {:close-btn? false
+                :center? true
+                :close-backdrop? (not= type :welcome)} opts)))))
 
 (defmethod handle :file-sync/maybe-onboarding-show [[_ type]]
   (file-sync/maybe-onboarding-show type))

+ 2 - 3
src/main/frontend/handler/file_based/property/util.cljs

@@ -208,8 +208,7 @@
   "Adds aliases to a page when a page has aliases and is also an alias of other pages"
   [properties page-id]
   (let [repo (state/get-current-repo)
-        aliases (db/get-page-alias-names repo
-                                         (:block/name (db/pull page-id)))]
+        aliases (db/get-page-alias-names repo page-id)]
     (if (seq aliases)
       (if (:alias properties)
         (update properties :alias (fn [c]
@@ -234,4 +233,4 @@
     (if (seq properties-order)
       (keep (fn [k] (when (contains? properties k) [k (get properties k)]))
             (distinct properties-order))
-      properties*)))
+      properties*)))

+ 1 - 2
src/main/frontend/worker/react.cljs

@@ -2,8 +2,7 @@
   "Compute reactive query affected keys"
   (:require [datascript.core :as d]
             [logseq.common.util :as common-util]
-            [cljs.spec.alpha :as s]
-            [logseq.db :as ldb]))
+            [cljs.spec.alpha :as s]))
 
 ;;; keywords specs for reactive query, used by `react/q` calls
 ;; ::block

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

@@ -161,6 +161,7 @@
              [:update (cond-> {:block-uuid block-uuid
                                :pos pos
                                :av-coll other-av-coll}
+                        (:db/ident block) (assoc :db/ident (:db/ident block))
                         card-one-attrs (assoc :card-one-attrs card-one-attrs))]))
     (when update-schema-op
       (swap! *remote-ops conj update-schema-op))

+ 1 - 10
src/main/frontend/worker/rtc/const.cljs

@@ -46,6 +46,7 @@
     [:cat :keyword
      [:map
       [:block-uuid :uuid]
+      [:db/ident {:optional true} :keyword]
       [:pos block-pos-schema]
       [:av-coll [:sequential av-schema]]
       [:card-one-attrs {:optional true} [:sequential :keyword]]]]]
@@ -213,16 +214,6 @@
       [:action :string]
       [:graph-uuid :string]
       [:block-uuids [:sequential :uuid]]]]
-    ["update-assets"
-     [:map
-      [:req-id :string]
-      [:action :string]
-      [:graph-uuid :uuid]
-      [:create {:optional true} [:sequential
-                                 [:map
-                                  [:asset-uuid :uuid]
-                                  [:asset-name :string]]]]
-      [:delete {:optional true} [:sequential :uuid]]]]
     ["calibrate-graph-skeleton"
      [:map
       [:req-id :string]

+ 9 - 3
src/main/frontend/worker/rtc/db_listener.cljs

@@ -23,14 +23,20 @@
   #{:block/content :block/created-at :block/updated-at :block/alias
     :block/tags :block/type :block/schema :block/link :block/journal-day
     :class/parent :class/schema.properties :property/schema.classes :property.value/content
-    :db/ident :db/index :db/valueType :db/cardinality})
+    :db/index :db/valueType :db/cardinality})
+
+(def ^:private watched-attr-ns
+  #{"logseq.property" "logseq.property.tldraw" "logseq.property.pdf" "logseq.task"
+    "logseq.property.linked-references"
+    "logseq.class" "logseq.kv"})
 
 (defn- watched-attr?
   [attr]
   (or (contains? watched-attrs attr)
       (let [ns (namespace attr)]
-        (or (= "logseq.task" ns)        ;e.g. :logseq.task/status
-            (string/ends-with? ns ".property"))))) ; :logseq.property/xxx, :user.property/xxx
+        (or (contains? watched-attr-ns ns)
+            (string/ends-with? ns ".property")
+            (string/ends-with? ns ".class")))))
 
 (defn- ref-attr?
   [db attr]

+ 1 - 2
src/test/frontend/db/db_based_model_test.cljs

@@ -4,8 +4,7 @@
             [frontend.db :as db]
             [frontend.test.helper :as test-helper]
             [datascript.core :as d]
-            [logseq.outliner.property :as outliner-property]
-            [logseq.db :as ldb]))
+            [logseq.outliner.property :as outliner-property]))
 
 (def repo test-helper/test-db-name-db-version)
 

+ 31 - 3
src/test/frontend/worker/rtc/client_test.cljs

@@ -9,6 +9,35 @@
 (def empty-db (d/empty-db db-schema/schema-for-db-based-graph))
 
 (deftest local-block-ops->remote-ops-test
+  (testing "user.class/yyy creation"
+    (let [block-uuid (random-uuid)
+          db (d/db-with empty-db [{:block/uuid block-uuid,
+                                   :block/updated-at 1720017595873,
+                                   :block/created-at 1720017595872,
+                                   :block/format :markdown,
+                                   :db/ident :user.class/yyy,
+                                   :block/type ["class"],
+                                   :block/name "yyy",
+                                   :block/original-name "yyy"}])]
+      (is (= [[:update
+               {:block-uuid block-uuid
+                :db/ident :user.class/yyy
+                :pos [nil nil],
+                :av-coll
+                [[:block/name "[\"~#'\",\"yyy\"]" 1 true]
+                 [:block/original-name "[\"~#'\",\"yyy\"]" 1 true]
+                 [:block/type "[\"~#'\",\"class\"]" 1 true]]}]]
+             (:remote-ops
+              (#'subject/local-block-ops->remote-ops
+               db
+               {:move [:move 1 {:block-uuid block-uuid}]
+                :update
+                [:update 1 {:block-uuid block-uuid
+                            :av-coll
+                            [[:block/name (ldb/write-transit-str "yyy") 1 true]
+                             [:block/original-name (ldb/write-transit-str "yyy") 1 true]
+                             [:block/type (ldb/write-transit-str "class") 1 true]]}]}))))))
+
   (testing "user.property/xxx creation"
     (let [block-uuid (random-uuid)
           block-order "b0P"
@@ -28,10 +57,10 @@
       (is (=
            [[:update
              {:block-uuid block-uuid,
+              :db/ident :user.property/xxx
               :pos [nil block-order],
               :av-coll
-              [[:db/ident "[\"~#'\",\"~:user.property/xxx\"]" 1 true]
-               [:block/name "[\"~#'\",\"xxx\"]" 1 true]
+              [[:block/name "[\"~#'\",\"xxx\"]" 1 true]
                [:block/original-name "[\"~#'\",\"xxx\"]" 1 true]
                [:block/type "[\"~#'\",\"property\"]" 1 true]]}]
             [:update-schema
@@ -48,7 +77,6 @@
               [:update 1 {:block-uuid block-uuid
                           :av-coll
                           [[:db/valueType (ldb/write-transit-str :db.type/ref) 1 true]
-                           [:db/ident (ldb/write-transit-str :user.property/xxx) 1 true]
                            [:block/name (ldb/write-transit-str "xxx") 1 true]
                            [:block/original-name (ldb/write-transit-str "xxx") 1 true]
                            [:block/type (ldb/write-transit-str "property") 1 true]

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

@@ -13,7 +13,6 @@
         id->same-entity-datoms (group-by first datom-vec-coll)]
     (update-vals id->same-entity-datoms #'worker-db-listener/entity-datoms=>a->add?->v->t)))
 
-
 (deftest entity-datoms=>ops-test
   (testing "remove whiteboard page-block"
     (let [conn (d/conn-from-db empty-db)
@@ -62,8 +61,39 @@
                        [:block/created-at "[\"~#'\",1716882111476]"]
                        [:block/schema "[\"^ \",\"~:type\",\"~:number\"]"]
                        [: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]]
+                  [op-type (cond-> op-value
+                             (:av-coll op-value)
+                             (assoc :av-coll (map #(take 2 %) (:av-coll op-value))))])
+                ops)))))
+
+  (testing "create user-class"
+    (let [conn (d/conn-from-db empty-db)
+          tx-data [[:db/add 62 :block/uuid #uuid "66856a29-6eb3-4122-af97-8580a853c6a6" 536870954]
+                   [:db/add 62 :block/updated-at 1720019497643 536870954]
+                   [:db/add 62 :class/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/original-name "zzz" 536870954]]
+          {:keys [db-before db-after tx-data]} (d/transact! conn tx-data)
+          ops (#'subject/entity-datoms=>ops db-before db-after
+                                            (tx-data=>e->a->add?->v->t tx-data)
+                                            (map vec tx-data))]
+      (is (=
+           [[:update-page {:block-uuid #uuid "66856a29-6eb3-4122-af97-8580a853c6a6"}]
+            [:update {:block-uuid #uuid "66856a29-6eb3-4122-af97-8580a853c6a6",
+                      :av-coll
+                      [[:block/updated-at "[\"~#'\",1720019497643]"]
+                       [:block/created-at "[\"~#'\",1720019497643]"]
+                       [:block/type "[\"~#'\",\"class\"]"]
+                       ;;1. no :class/parent, because db/id 4 block doesn't exist in empty-db
+                       ;;2. shouldn't have :db/ident, :db/ident is special, will be handled later
+                       ]}]]
            (map (fn [[op-type _t op-value]]
                   [op-type (cond-> op-value
                              (:av-coll op-value)