Kaynağa Gözat

enhance: add EDN export for any view

Gabriel Horner 7 ay önce
ebeveyn
işleme
1d855d012f

+ 65 - 15
deps/db/src/logseq/db/sqlite/export.cljs

@@ -231,6 +231,21 @@
       (seq classes)
       (assoc :classes classes))))
 
+(defn- build-mixed-properties-and-classes-export
+  "Builds an export of properties and classes from a mixed group of nodes that may both"
+  [db ents export-opts]
+  (let [properties
+        (when-let [prop-ids (seq (map :db/ident (filter entity-util/property? ents)))]
+          (build-export-properties db prop-ids export-opts))
+        classes
+        (when-let [class-ents (seq (filter ldb/class? ents))]
+          (->> class-ents
+               (map #(vector (:db/ident %) (build-export-class % export-opts)))
+               (into {})))]
+    (cond-> {}
+      properties (assoc :properties properties)
+      classes (assoc :classes classes))))
+
 (defn- build-content-ref-export
   "Builds an export config (and additional info) for refs in the given blocks. All the exported
    entities found in block refs include their uuid in order to preserve the relationship to the blocks"
@@ -239,20 +254,13 @@
         blocks (remove :logseq.property/value blocks*)
         content-ref-uuids (set (mapcat (comp db-content/get-matched-ids block-title) blocks))
         content-ref-ents (map #(d/entity db [:block/uuid %]) content-ref-uuids)
-        content-ref-pages (filter #(or (ldb/internal-page? %) (entity-util/journal? %)) content-ref-ents)
-        content-ref-properties (when-let [prop-ids (seq (map :db/ident (filter ldb/property? content-ref-ents)))]
-                                 (update-vals (build-export-properties db prop-ids {:include-uuid? true :shallow-copy? true})
-                                              #(merge % {:build/keep-uuid? true})))
-        content-ref-classes (when-let [class-ents (seq (filter ldb/class? content-ref-ents))]
-                              (->> class-ents
-                                   (map #(vector (:db/ident %)
-                                                 (assoc (build-export-class % {:include-uuid? true :shallow-copy? true})
-                                                        :build/keep-uuid? true)))
-                                   (into {})))]
+        content-ref-pages (filter #(or (entity-util/internal-page? %) (entity-util/journal? %)) content-ref-ents)
+        {:keys [properties classes]}
+        (build-mixed-properties-and-classes-export db content-ref-ents {:include-uuid? true :shallow-copy? true})]
     {:content-ref-uuids content-ref-uuids
      :content-ref-ents content-ref-ents
-     :properties content-ref-properties
-     :classes content-ref-classes
+     :properties (update-vals properties #(merge % {:build/keep-uuid? true}))
+     :classes (update-vals classes #(merge % {:build/keep-uuid? true}))
      :pages-and-blocks (mapv #(hash-map :page (merge (shallow-copy-page %)
                                                      {:block/uuid (:block/uuid %) :build/keep-uuid? true}))
                              content-ref-pages)}))
@@ -278,14 +286,14 @@
      :properties properties}))
 
 (defn- build-blocks-export
-  "Given a page's block entities, returns the blocks in a sqlite.build EDN format
+  "Given a vec of block entities, returns the blocks in a sqlite.build EDN format
    and all properties and classes used in these blocks"
-  [db blocks opts]
+  [db blocks {:keys [include-children?] :or {include-children? true} :as opts}]
   (let [*properties (atom {})
         *classes (atom {})
         *pvalue-uuids (atom #{})
         id-map (into {} (map (juxt :db/id identity)) blocks)
-        children (group-by #(get-in % [:block/parent :db/id]) blocks)
+        children (if include-children? (group-by #(get-in % [:block/parent :db/id]) blocks) {})
         build-block (fn build-block [block*]
                       (let [child-nodes (mapv build-block (get children (:db/id block*) []))
                             {:keys [node properties classes]}
@@ -377,6 +385,46 @@
         page-export (finalize-export-maps db page-blocks-export uuid-block-export content-ref-export)]
     page-export))
 
+(defn build-view-nodes-export* [db nodes opts]
+  (let [node-pages (filter entity-util/page? nodes)
+        pages-export
+        (merge
+         (build-mixed-properties-and-classes-export db node-pages {:shallow-copy? true})
+         {:pages-and-blocks (mapv #(hash-map :page (shallow-copy-page %))
+                                  (filter #(or (entity-util/internal-page? %) (entity-util/journal? %)) node-pages))})
+        node-blocks (remove entity-util/page? nodes)
+        ;; Similar to build-uuid-block-export
+        pages-to-blocks
+        (->> node-blocks
+             (group-by :block/parent)
+             (map (fn [[parent-page-ent blocks]]
+                    (merge (build-blocks-export db
+                                                (sort-by :block/order blocks)
+                                                (merge opts {:include-children? false}))
+                           {:page (shallow-copy-page parent-page-ent)}))))
+        pages-to-blocks-export
+        {:properties (apply merge (map :properties pages-to-blocks))
+         :classes (apply merge (map :classes pages-to-blocks))
+         :pages-and-blocks (mapv #(select-keys % [:page :blocks]) pages-to-blocks)}]
+    (merge (merge-export-maps pages-export pages-to-blocks-export)
+           {:pvalue-uuids (apply set/union (map :pvalue-uuids pages-to-blocks))})))
+
+(defn- build-view-nodes-export
+  "Exports given nodes from a view. Nodes are a random mix of blocks and pages"
+  [db eids]
+  (let [nodes (map #(d/entity db %) eids)
+        property-value-ents (mapcat #(->> (dissoc (db-property/properties %) :block/tags)
+                                          vals
+                                          (filter de/entity?))
+                                    nodes)
+        {:keys [content-ref-uuids content-ref-ents] :as content-ref-export}
+        (build-content-ref-export db (into nodes property-value-ents))
+        {:keys [pvalue-uuids] :as nodes-export}
+        (build-view-nodes-export* db nodes {:include-uuid-fn content-ref-uuids})
+        uuid-block-export (build-uuid-block-export db pvalue-uuids content-ref-ents {})
+        view-nodes-export (finalize-export-maps db nodes-export uuid-block-export content-ref-export)]
+    view-nodes-export))
+
 (defn- build-graph-ontology-export
   "Exports a graph's tags and properties"
   [db]
@@ -467,6 +515,8 @@
           (build-block-export db (:block-id options))
           :page
           (build-page-export db (:page-id options))
+          :view-nodes
+          (build-view-nodes-export db (:node-ids options))
           :graph-ontology
           (build-graph-ontology-export db))]
     (ensure-export-is-valid (dissoc export-map ::block))

+ 47 - 8
deps/db/test/logseq/db/sqlite/export_test.cljs

@@ -11,6 +11,13 @@
 
 ;; Test helpers
 ;; ============
+(defn- validate-db
+  "Validate db, usually after transacting an import"
+  [db]
+  (let [validation (db-validate/validate-db! db)]
+    (when (seq (:errors validation)) (cljs.pprint/pprint {:validate (:errors validation)}))
+    (is (empty? (map :entity (:errors validation))) "Imported graph has no validation errors")))
+
 (defn- export-block-and-import-to-another-block
   "Exports given block from one graph/conn, imports it to a 2nd block and then
    exports the 2nd block. The two blocks do not have to be in the same graph"
@@ -23,10 +30,8 @@
             (sqlite-export/build-import @import-conn {:current-block import-block}))
         ;; _ (cljs.pprint/pprint _txs)
         _ (d/transact! import-conn init-tx)
-        _ (d/transact! import-conn block-props-tx)
-        validation (db-validate/validate-db! @import-conn)
-        _ (when (seq (:errors validation)) (cljs.pprint/pprint {:validate (:errors validation)}))
-        _  (is (empty? (map :entity (:errors validation))) "Imported graph has no validation errors")]
+        _ (d/transact! import-conn block-props-tx)]
+    (validate-db @import-conn)
     (sqlite-export/build-export @import-conn {:export-type :block
                                               :block-id (:db/id import-block)})))
 
@@ -79,7 +84,7 @@
           [{:page {:build/journal 20250220}
             :blocks [{:block/title "b1"}]}
            {:page {:build/journal 20250221}}]}
-       (#'sqlite-export/merge-export-maps
+         (#'sqlite-export/merge-export-maps
           {:pages-and-blocks
            [{:page {:build/journal 20250220}
              :blocks [{:block/title "b1"}]}]}
@@ -180,9 +185,7 @@
         ;; _ (cljs.pprint/pprint _txs)
         _ (d/transact! import-conn init-tx)
         _ (d/transact! import-conn block-props-tx)
-        validation (db-validate/validate-db! @import-conn)
-        _ (when (seq (:errors validation)) (cljs.pprint/pprint {:validate (:errors validation)}))
-        _  (is (empty? (map :entity (:errors validation))) "Imported graph has no validation errors")
+        _ (validate-db @import-conn)
         page2 (db-test/find-page-by-title @import-conn page-title)]
     (sqlite-export/build-export @import-conn {:export-type :page :page-id (:db/id page2)})))
 
@@ -442,7 +445,43 @@
         ;; _ (cljs.pprint/pprint _txs)
         _ (d/transact! conn2 init-tx)
         _ (d/transact! conn2 block-props-tx)
+        _ (validate-db @conn2)
         imported-ontology (sqlite-export/build-export @conn2 {:export-type :graph-ontology})]
 
     (is (= (expand-properties (:properties original-data)) (:properties imported-ontology)))
     (is (= (expand-classes (:classes original-data)) (:classes imported-ontology)))))
+
+(deftest import-view-blocks
+  (let [original-data
+        ;; Test a mix of page and block types
+        {:properties {:user.property/p1 {:logseq.property/type :default}
+                      :user.property/p2 {:logseq.property/type :default}}
+         :classes {:user.class/class1 {}}
+         :pages-and-blocks [{:page {:block/title "page1"}}
+                            {:page {:build/journal 20250226}}
+                            {:page {:block/title "page2"}
+                             :blocks [{:block/title "b1"
+                                       :build/properties {:user.property/p2 "ok"}}]}]}
+        conn (db-test/create-conn-with-blocks original-data)
+        get-node-ids (fn [db]
+                       (->> [(d/entity db :user.property/p1)
+                             (d/entity db :user.class/class1)
+                             (db-test/find-page-by-title db "page1")
+                             (db-test/find-journal-by-journal-day db 20250226)
+                             (db-test/find-block-by-content db "b1")]
+                            (remove nil?)
+                            (mapv #(vector :block/uuid (:block/uuid %)))))
+        conn2 (db-test/create-conn)
+        {:keys [init-tx block-props-tx] :as _txs}
+        (-> (sqlite-export/build-export @conn {:export-type :view-nodes :node-ids (get-node-ids @conn)})
+            (sqlite-export/build-import @conn2 {}))
+        ;; _ (cljs.pprint/pprint _txs)
+        _ (d/transact! conn2 init-tx)
+        _ (d/transact! conn2 block-props-tx)
+        _ (validate-db @conn2)
+        imported-nodes (sqlite-export/build-export @conn2 {:export-type :view-nodes
+                                                           :node-ids (get-node-ids @conn2)})]
+
+    (is (= (:pages-and-blocks original-data) (:pages-and-blocks imported-nodes)))
+    (is (= (expand-properties (:properties original-data)) (:properties imported-nodes)))
+    (is (= (expand-classes (:classes original-data)) (:classes imported-nodes)))))

+ 7 - 2
src/main/frontend/components/views.cljs

@@ -19,6 +19,7 @@
             [frontend.db :as db]
             [frontend.db-mixins :as db-mixins]
             [frontend.db.async :as db-async]
+            [frontend.handler.db-based.export :as db-export-handler]
             [frontend.handler.db-based.property :as db-property-handler]
             [frontend.handler.editor :as editor-handler]
             [frontend.handler.property :as property-handler]
@@ -315,7 +316,7 @@
     columns))
 
 (rum/defc more-actions
-  [view-entity columns {:keys [column-visible? column-toggle-visibility]}]
+  [view-entity columns {:keys [column-visible? rows column-toggle-visibility]}]
   (let [display-type (:db/ident (:logseq.property.view/type view-entity))
         table? (= display-type :logseq.property.view/type.table)
         columns' (filter (fn [column]
@@ -366,7 +367,11 @@
                                                                                (:db/id (db/entity (:id column))))
                                       (db-property-handler/remove-block-property! (:db/id view-entity) :logseq.property.view/group-by-property)))
                  :onSelect (fn [e] (.preventDefault e))}
-                (:name column))))))))))))
+                (:name column))))))
+         (shui/dropdown-menu-item
+          {:key "export-edn"
+           :on-click #(db-export-handler/export-view-nodes-data rows)}
+          "Export EDN")))))))
 
 (defn- get-column-size
   [column sized-columns]

+ 12 - 0
src/main/frontend/handler/db_based/export.cljs

@@ -27,6 +27,18 @@
         (notification/show! "Copied block's data!" :success)))
     (notification/show! "No block found" :warning)))
 
+(defn export-view-nodes-data [nodes]
+  (let [block-uuids (mapv #(vector :block/uuid (:block/uuid %)) nodes)]
+    (when-let [^Object worker @state/*db-worker]
+      (p/let [result* (.export-edn worker
+                                   (state/get-current-repo)
+                                   (ldb/write-transit-str {:export-type :view-nodes :node-ids block-uuids}))
+              result (ldb/read-transit-str result*)
+              pull-data (with-out-str (pprint/pprint result))]
+        (.writeText js/navigator.clipboard pull-data)
+        (println pull-data)
+        (notification/show! "Copied block's data!" :success)))))
+
 (defn ^:export export-page-data []
   (if-let [page-id (page-util/get-current-page-id)]
     (when-let [^Object worker @state/*db-worker]