Browse Source

Merge branch 'feat/db' into refactor/db-properties-schema

Tienson Qin 1 year ago
parent
commit
7084b53dad

+ 74 - 8
.github/workflows/build-desktop-release.yml

@@ -222,7 +222,7 @@ jobs:
           DEBUG: "pw:api"
           RELEASE: true # skip dev only test
 
-  build-linux:
+  build-linux-x64:
     runs-on: ubuntu-20.04
     needs: [ compile-cljs ]
     steps:
@@ -265,7 +265,61 @@ jobs:
       - name: Upload Artifact
         uses: actions/upload-artifact@v3
         with:
-          name: logseq-linux-builds
+          name: logseq-linux-x64-builds
+          path: builds
+
+  build-linux-arm64:
+    runs-on: ubuntu-20.04
+    needs: [ compile-cljs ]
+    steps:
+      - name: Download The Static Asset
+        uses: actions/download-artifact@v3
+        with:
+          name: static
+          path: static
+
+      - name: Retrieve tag version
+        id: ref
+        run: |
+          pkgver=$(cat ./static/VERSION)
+          echo "version=$pkgver" >> $GITHUB_OUTPUT
+
+      - name: Install Node.js, NPM and Yarn
+        uses: actions/setup-node@v3
+        with:
+          node-version: ${{ env.NODE_VERSION }}
+
+      - name: Fetch deps
+        env:
+          npm_config_arch: arm64
+        run: |
+          yarn install --target_arch=arm64 --target_platform=linux
+          rsapi_version=`node -e 'console.log(require("@logseq/rsapi/package.json").optionalDependencies["@logseq/rsapi-linux-arm64-gnu"])'`
+          temp_dir=`mktemp -d`
+          cd "$temp_dir"
+          echo '{"dependencies": {"@logseq/rsapi-linux-arm64-gnu": "'"$rsapi_version"'"}}' > package.json
+          yarn install --ignore-platform
+          cd -
+          mv "$temp_dir/node_modules/@logseq/rsapi-linux-arm64-gnu" node_modules/@logseq/rsapi-linux-arm64-gnu
+          rm -rf "$temp_dir" "node_modules/@logseq/rsapi-linux-x64-gnu"
+        working-directory: ./static
+
+      - name: Build/Release Electron App
+        run: yarn electron:make-linux-arm64
+        working-directory: ./static
+
+      - name: Save artifacts
+        run: |
+          mkdir -p builds
+          # NOTE: save VERSION file to builds directory
+          cp static/VERSION ./builds/VERSION
+          # mv static/out/make/*-*.AppImage ./builds/Logseq-linux-arm64-${{ steps.ref.outputs.version }}.AppImage
+          mv static/out/make/zip/linux/arm64/*-linux-arm64-*.zip ./builds/Logseq-linux-arm64-${{ steps.ref.outputs.version }}.zip
+
+      - name: Upload Artifact
+        uses: actions/upload-artifact@v3
+        with:
+          name: logseq-linux-arm64-builds
           path: builds
 
   build-windows:
@@ -498,7 +552,7 @@ jobs:
 
   nightly-release:
     if: ${{ github.event_name == 'schedule' || github.event.inputs.build-target == 'nightly' }}
-    needs: [ build-macos-x64, build-macos-arm64, build-linux, build-windows, build-android, e2e-test ]
+    needs: [ build-macos-x64, build-macos-arm64, build-linux-x64, build-linux-arm64, build-windows, build-android, e2e-test ]
     runs-on: ubuntu-20.04
     steps:
       - name: Download MacOS x64 Artifacts
@@ -513,10 +567,16 @@ jobs:
           name: logseq-darwin-arm64-builds
           path: ./
 
-      - name: Download The Linux Artifacts
+      - name: Download The Linux x64 Artifacts
         uses: actions/download-artifact@v3
         with:
-          name: logseq-linux-builds
+          name: logseq-linux-x64-builds
+          path: ./
+
+      - name: Download The Linux arm64 Artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: logseq-linux-arm64-builds
           path: ./
 
       - name: Download The Windows Artifact
@@ -565,7 +625,7 @@ jobs:
   release:
     # NOTE: For now, we only have beta channel to be released on Github
     if: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.build-target == 'beta' }}
-    needs: [ build-macos-x64, build-macos-arm64, build-linux, build-windows, e2e-test ]
+    needs: [ build-macos-x64, build-macos-arm64, build-linux-x64, build-linux-arm64, build-windows, e2e-test ]
     runs-on: ubuntu-20.04
     steps:
       - name: Download MacOS x64 Artifacts
@@ -580,10 +640,16 @@ jobs:
           name: logseq-darwin-arm64-builds
           path: ./
 
-      - name: Download The Linux Artifacts
+      - name: Download The Linux x64 Artifacts
+        uses: actions/download-artifact@v3
+        with:
+          name: logseq-linux-x64-builds
+          path: ./
+
+      - name: Download The Linux arm64 Artifacts
         uses: actions/download-artifact@v3
         with:
-          name: logseq-linux-builds
+          name: logseq-linux-arm64-builds
           path: ./
 
       - name: Download The Windows Artifact

+ 108 - 0
deps/graph-parser/src/logseq/graph_parser/exporter.cljs

@@ -13,6 +13,7 @@
             [logseq.db.frontend.property :as db-property]
             [logseq.db.frontend.property.type :as db-property-type]
             [logseq.common.util.macro :as macro-util]
+            [logseq.common.util.date-time :as date-time-util]
             [logseq.db.sqlite.util :as sqlite-util]
             [logseq.db :as ldb]
             [logseq.db.frontend.rules :as rules]
@@ -110,6 +111,108 @@
                     (keep #(convert-tag-to-class % 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]}]
+  (if-let [marker (:block/marker block)]
+    (let [old-to-new {"TODO" :logseq.task/status.todo
+                      "LATER" :logseq.task/status.todo
+                      "IN-PROGRESS" :logseq.task/status.doing
+                      "NOW" :logseq.task/status.doing
+                      "DOING" :logseq.task/status.doing
+                      "DONE" :logseq.task/status.done
+                      "WAIT" :logseq.task/status.backlog
+                      "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))]
+      (-> block
+          (update :block/properties assoc status-prop status-value)
+          (update :block/content string/replace-first (re-pattern (str marker "\\s*")) "")
+          (update :block/tags (fnil conj []) :logseq.class/task)
+          (update :block/refs (fn [refs]
+                                (into (remove #(= marker (:block/original-name %)) refs)
+                                      [:logseq.class/task :logseq.task/status status-ident])))
+          (update :block/path-refs (fn [refs]
+                                     (into (remove #(= marker (:block/original-name %)) refs)
+                                           [:logseq.class/task :logseq.task/status status-ident])))
+          (dissoc :block/marker)))
+    block))
+
+(defn- update-block-priority
+  [block db {: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)
+                             (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))]
+      (-> block
+          (update :block/properties assoc priority-prop 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])))
+          (update :block/path-refs (fn [refs]
+                                     (into (remove #(= priority (:block/original-name %)) refs)
+                                           [:logseq.task/priority priority-ident])))
+          (dissoc :block/priority)))
+    block))
+
+(defn- update-block-deadline
+  ":block/content doesn't contain DEADLINE.* text so unable to detect timestamp
+  or repeater usage and notify user that they aren't supported"
+  [block db {:keys [user-config]}]
+  (if-let [deadline (:block/deadline block)]
+    (let [deadline-prop (:block/uuid (d/entity db :logseq.task/deadline))
+          deadline-page (or (ffirst (d/q '[:find (pull ?b [:block/uuid])
+                                           :in $ ?journal-day
+                                           :where [?b :block/journal-day ?journal-day]]
+                                         db deadline))
+                            ;; FIXME: Register new pages so that two different refs to same new page
+                            ;; don't create different uuids and thus an invalid page
+                            (assoc (sqlite-util/build-new-page
+                                    (date-time-util/int->journal-title deadline (common-config/get-date-formatter user-config)))
+                                   :block/journal? true
+                                   :block/journal-day deadline
+                                   :block/format :markdown))]
+      (-> block
+          (update :block/properties assoc deadline-prop (:block/uuid deadline-page))
+          (update :block/refs (fnil into []) [:logseq.task/deadline deadline-page])
+          (update :block/path-refs (fnil into []) [:logseq.task/deadline deadline-page])
+          (dissoc :block/deadline)))
+    block))
+
+(defn- update-block-scheduled
+  "Should have same implementation as update-block-deadline"
+  [block db {:keys [user-config]}]
+  (if-let [scheduled (:block/scheduled block)]
+    (let [scheduled-prop (:block/uuid (d/entity db :logseq.task/scheduled))
+          scheduled-page (or (ffirst (d/q '[:find (pull ?b [:block/uuid])
+                                           :in $ ?journal-day
+                                           :where [?b :block/journal-day ?journal-day]]
+                                         db scheduled))
+                            (assoc (sqlite-util/build-new-page
+                                    (date-time-util/int->journal-title scheduled (common-config/get-date-formatter user-config)))
+                                   :block/journal? true
+                                   :block/journal-day scheduled
+                                   :block/format :markdown))]
+      (-> block
+          (update :block/properties assoc scheduled-prop (:block/uuid scheduled-page))
+          (update :block/refs (fnil into []) [:logseq.task/scheduled scheduled-page])
+          (update :block/path-refs (fnil into []) [:logseq.task/scheduled scheduled-page])
+          (dissoc :block/scheduled)))
+    block))
+
 (defn- text-with-refs?
   "Detects if a property value has text with refs e.g. `#Logseq is #awesome`
   instead of `#Logseq #awesome`. If so the property type is :default instead of :page"
@@ -448,6 +551,10 @@
         (handle-block-properties db page-names-to-uuids (:block/refs block) options)
         (update-block-refs page-names-to-uuids old-property-schemas options)
         (update-block-tags tag-classes page-names-to-uuids)
+        (update-block-marker db options)
+        (update-block-priority db options)
+        (update-block-deadline db options)
+        (update-block-scheduled db options)
         add-missing-timestamps
         ;; ((fn [x] (prn :block-out x) x))
         ;; TODO: org-mode content needs to be handled
@@ -831,6 +938,7 @@
                          :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)

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

@@ -931,9 +931,10 @@
        (remove nil?)))))
 
 (defn ^:api delete-block
-  "Delete block from the tree."
+  "FIXME: why expose this fn? there's already a public fn `delete-blocks!`
+  Delete block from the tree."
   [repo conn txs-state node {:keys [children? children-check? date-formatter]
-                        :or {children-check? true}}]
+                             :or {children-check? true}}]
   (if (and children-check?
            (not children?)
            (first (:block/_parent (d/entity @conn [:block/uuid (:block/uuid (get-data node))]))))

+ 2 - 1
resources/package.json

@@ -11,6 +11,7 @@
     "electron:dev": "electron-forge start",
     "electron:debug": "electron-forge start --inspect-electron",
     "electron:make": "electron-forge make",
+    "electron:make-linux-arm64": "electron-forge make --platform=linux --arch=arm64",
     "electron:make-macos-arm64": "electron-forge make --platform=darwin --arch=arm64",
     "electron:publish:github": "electron-forge publish",
     "rebuild:all": "electron-rebuild -v 27.1.3 -f",
@@ -28,7 +29,7 @@
     "chokidar": "^3.5.1",
     "command-exists": "1.2.9",
     "diff-match-patch": "1.0.5",
-    "dugite": "2.5.0",
+    "dugite": "2.5.1",
     "electron-deeplink": "1.0.10",
     "electron-dl": "3.3.0",
     "electron-log": "4.3.1",

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

@@ -10,6 +10,7 @@
             [datascript.core :as d]
             [datascript.storage :refer [IStorage]]
             [frontend.worker.async-util :include-macros true :refer [<?] :as async-util]
+            [frontend.worker.db-listener :as db-listener]
             [frontend.worker.db-metadata :as worker-db-metadata]
             [frontend.worker.export :as worker-export]
             [frontend.worker.file :as file]
@@ -22,6 +23,7 @@
             [frontend.worker.rtc.snapshot :as rtc-snapshot]
             [frontend.worker.search :as search]
             [frontend.worker.state :as worker-state]
+            [frontend.worker.undo-redo]
             [frontend.worker.util :as worker-util]
             [logseq.db :as ldb]
             [logseq.db.sqlite.common-db :as sqlite-common-db]
@@ -174,7 +176,9 @@
             conn (sqlite-common-db/get-storage-conn storage schema)]
         (swap! *datascript-conns assoc repo conn)
         (p/let [_ (op-mem-layer/<init-load-from-indexeddb! repo)]
-          (rtc-db-listener/listen-to-db-changes! repo conn))))))
+          (rtc-db-listener/listen-to-db-changes! repo conn)
+          (db-listener/listen-db-changes! repo conn))
+        ))))
 
 (defn- iter->vec [iter]
   (when iter

+ 2 - 2
src/main/frontend/util.cljc

@@ -718,7 +718,7 @@
              (js/console.error e)
              (dec current-pos)))
          (dec current-pos))
-       (dec current-pos))))
+       current-pos)))
 
 #?(:cljs
    ;; for widen char
@@ -735,7 +735,7 @@
              (js/console.error e)
              (inc current-pos)))
          (inc current-pos))
-       (inc current-pos))))
+       current-pos)))
 
 #?(:cljs
    (defn kill-line-before!

+ 46 - 0
src/main/frontend/worker/db_listener.cljs

@@ -0,0 +1,46 @@
+(ns frontend.worker.db-listener
+  "Db listeners for worker-db."
+  (:require [datascript.core :as d]))
+
+
+(defn- entity-datoms=>attr->datom
+  [entity-datoms]
+  (reduce
+   (fn [m datom]
+     (let [[_e a _v t add?] datom]
+       (if-let [[_e _a _v old-t old-add?] (get m a)]
+         (cond
+           (and (= old-t t)
+                (true? add?)
+                (false? old-add?))
+           (assoc m a datom)
+
+           (< old-t t)
+           (assoc m a datom)
+
+           :else
+           m)
+         (assoc m a datom))))
+   {} entity-datoms))
+
+
+(defmulti listen-db-changes
+  (fn [listen-key & _] listen-key))
+
+(defn listen-db-changes!
+  [repo conn]
+  (let [handlers (methods listen-db-changes)]
+    (prn :listen-db-changes! (keys handlers))
+    (d/unlisten! conn ::listen-db-changes!)
+    (d/listen! conn ::listen-db-changes!
+               (fn [{:keys [tx-data] :as args}]
+                 (let [datom-vec-coll (map vec tx-data)
+                       id->same-entity-datoms (group-by first datom-vec-coll)
+                       id-order (distinct (map first datom-vec-coll))
+                       same-entity-datoms-coll (map id->same-entity-datoms id-order)
+                       id->attr->datom (update-vals id->same-entity-datoms entity-datoms=>attr->datom)]
+                   (doseq [[k handler-fn] handlers]
+                     (handler-fn k (assoc args
+                                          :repo repo
+                                          :id->attr->datom id->attr->datom
+                                          :same-entity-datoms-coll same-entity-datoms-coll))))))))

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

@@ -18,10 +18,18 @@
   [entity-datoms]
   (reduce
    (fn [m datom]
-     (let [[_e a _v t _add?] datom]
-       (if-let [[_e _a _v old-t _old-add?] (get m a)]
-         (if (<= old-t t)
+     (let [[_e a _v t add?] datom]
+       (if-let [[_e _a _v old-t old-add?] (get m a)]
+         (cond
+           (and (= old-t t)
+                (true? add?)
+                (false? old-add?))
            (assoc m a datom)
+
+           (< old-t t)
+           (assoc m a datom)
+
+           :else
            m)
          (assoc m a datom))))
    {} entity-datoms))

+ 5 - 1
src/main/frontend/worker/state.cljs

@@ -8,11 +8,15 @@
                        :db/latest-transact-time {}
                        :worker/context {}
 
+                       ;; FIXME: this name :config is too general
                        :config {}
                        :git/current-repo nil
                        :rtc/batch-processing? false
                        :rtc/remote-batch-txs nil
-                       :rtc/downloading-graph? false}))
+                       :rtc/downloading-graph? false
+
+                       :undo/repo->undo-stack (atom {})
+                       :undo/repo->redo-stack (atom {})}))
 
 (defonce *rtc-ws-url (atom nil))
 

+ 239 - 27
src/main/frontend/worker/undo_redo.cljs

@@ -1,35 +1,52 @@
 (ns frontend.worker.undo-redo
   "undo/redo related fns and op-schema"
-  (:require [datascript.core :as d]))
-
+  (:require [datascript.core :as d]
+            [frontend.worker.db-listener :as db-listener]
+            [frontend.worker.state :as worker-state]
+            [logseq.common.config :as common-config]
+            [logseq.outliner.core :as outliner-core]
+            [logseq.outliner.transaction :as outliner-tx]
+            [malli.core :as m]
+            [malli.util :as mu]))
 
 (def undo-op-schema
-  [:multi {:dispatch first}
-   [:boundary
-    [:cat :keyword]]
-   [:insert-block
-    [:cat :keyword
-     [:map
-      [:block-uuid :uuid]]]]
-   [:move-block
-    [:cat :keyword
-     [:map
-      [:block-uuid :uuid]
-      [:block-origin-left :uuid]
-      [:block-origin-parent :uuid]]]]
-   [:remove-block
-    [:cat :keyword
-     [:map
-      [:block-uuid :uuid]
-      [:block-entity-map :map]]]]
-   [:update-block
-    [:cat :keyword
-     [:map
-      [:block-uuid :uuid]
-      [:block-origin-content {:optional true} :string]
-      ;; TODO: add more attrs
-      ]]]])
+  (mu/closed-schema
+   [:multi {:dispatch first}
+    [:boundary
+     [:cat :keyword]]
+    [:insert-block
+     [:cat :keyword
+      [:map
+       [:block-uuid :uuid]]]]
+    [:move-block
+     [:cat :keyword
+      [:map
+       [:block-uuid :uuid]
+       [:block-origin-left :uuid]
+       [:block-origin-parent :uuid]]]]
+    [:remove-block
+     [:cat :keyword
+      [:map
+       [:block-uuid :uuid]
+       [:block-entity-map
+        [:map
+         [:block/uuid :uuid]
+         [:block/left :uuid]
+         [:block/parent :uuid]
+         [:block/content :string]
+         [:block/created-at :int]
+         [:block/updated-at :int]
+         [:block/format :any]
+         [:block/tags {:optional true} [:sequential :uuid]]]]]]]
+    [:update-block
+     [:cat :keyword
+      [:map
+       [:block-uuid :uuid]
+       [:block-origin-content {:optional true} :string]
+       ;; TODO: add more attrs
+       ]]]]))
 
+(def undo-ops-validator (m/validator [:sequential undo-op-schema]))
 
 (defn reverse-op
   [db op]
@@ -68,3 +85,198 @@
         [:update-block
          (cond-> {:block-uuid block-uuid}
            block-origin-content (assoc :block-origin-content block-origin-content))]))))
+
+
+(def ^:private apply-conj-vec (partial apply (fnil conj [])))
+
+(defn- push-undo-ops
+  [repo ops]
+  (swap! (:undo/repo->undo-stack @worker-state/*state) update repo apply-conj-vec ops))
+
+(defn- pop-undo-op
+  [repo]
+  (let [repo->undo-stack (:undo/repo->undo-stack @worker-state/*state)]
+    (when-let [peek-op (peek (@repo->undo-stack repo))]
+      (swap! repo->undo-stack update repo pop)
+      peek-op)))
+
+(defn- push-redo-ops
+  [repo ops]
+  (swap! (:undo/repo->redo-stack @worker-state/*state) update repo apply-conj-vec ops))
+
+(defn- pop-redo-op
+  [repo]
+  (let [repo->redo-stack (:undo/repo->redo-stack @worker-state/*state)]
+    (when-let [peek-op (peek (@repo->redo-stack repo))]
+      (swap! repo->redo-stack update repo pop)
+      peek-op)))
+
+
+(defmulti reverse-apply-op (fn [op _conn _repo] (first op)))
+(defmethod reverse-apply-op :remove-block
+  [op conn repo]
+  (let [[_ {:keys [block-uuid block-entity-map]}] op]
+    (when-let [left-entity (d/entity @conn [:block/uuid (:block/left block-entity-map)])]
+      (let [sibling? (not= (:block/left block-entity-map) (:block/parent block-entity-map))]
+        (outliner-tx/transact!
+         {:gen-undo-op? false
+          :outliner-op :insert-blocks
+          :transact-opts {:repo repo
+                          :conn conn}}
+         (outliner-core/insert-blocks! repo conn
+                                       [(cond-> {:block/uuid block-uuid
+                                                 :block/content (:block/content block-entity-map)
+                                                 :block/created-at (:block/created-at block-entity-map)
+                                                 :block/updated-at (:block/updated-at block-entity-map)
+                                                 :block/format :markdown}
+                                          (seq (:block/tags block-entity-map))
+                                          (assoc :block/tags (mapv (partial vector :block/uuid)
+                                                                   (:block/tags block-entity-map))))]
+                                       left-entity {:sibling? sibling? :keep-uuid? true}))
+        :push-undo-redo
+        ))))
+
+(defmethod reverse-apply-op :insert-block
+  [op conn repo]
+  (let [[_ {:keys [block-uuid]}] op]
+    (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])]
+      (when (empty? (seq (:block/_parent block-entity))) ;if have children, skip
+        (outliner-tx/transact!
+         {:gen-undo-op? false
+          :outliner-op :delete-blocks
+          :transact-opts {:repo repo
+                          :conn conn}}
+         (outliner-core/delete-blocks! repo conn
+                                       (common-config/get-date-formatter (worker-state/get-config repo))
+                                       [block-entity]
+                                       {:children? false}))
+        :push-undo-redo))))
+
+(defmethod reverse-apply-op :move-block
+  [op conn repo]
+  (let [[_ {:keys [block-uuid block-origin-left block-origin-parent]}] op]
+    (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])]
+      (when-let [left-entity (d/entity @conn [:block/uuid block-origin-left])]
+        (let [sibling? (not= block-origin-left block-origin-parent)]
+          (outliner-tx/transact!
+           {:gen-undo-op? false
+            :outliner-op :move-blocks
+            :transact-opts {:repo repo
+                            :conn conn}}
+           (outliner-core/move-blocks! repo conn [block-entity] left-entity sibling?))
+          :push-undo-redo)))))
+
+(defmethod reverse-apply-op :update-block
+  [op conn repo]
+  (let [[_ {:keys [block-uuid block-origin-content]}] op]
+    (when-let [block-entity (d/entity @conn [:block/uuid block-uuid])]
+      (let [new-block (assoc block-entity :block/content block-origin-content)]
+        (outliner-tx/transact!
+         {:gen-undo-op? false
+          :outliner-op :save-block
+          :transact-opts {:repo repo
+                          :conn conn}}
+         (outliner-core/save-block! repo conn
+                                    (common-config/get-date-formatter (worker-state/get-config repo))
+                                    new-block))
+        :push-undo-redo))))
+
+
+(defn undo
+  [repo]
+  (when-let [op (pop-undo-op repo)]
+    (let [conn (worker-state/get-datascript-conn repo)
+          rev-op (reverse-op @conn op)]
+      (when (= :push-undo-redo (reverse-apply-op op conn repo))
+        (push-redo-ops repo [rev-op])))))
+
+(defn redo
+  [repo]
+  (when-let [op (pop-redo-op repo)]
+    (let [conn (worker-state/get-datascript-conn repo)
+          rev-op (reverse-op @conn op)]
+      (when (= :push-undo-redo (reverse-apply-op op conn repo))
+        (push-undo-ops repo [rev-op])))))
+
+
+;;; listen db changes and push undo-ops
+
+(def ^:private entity-map-pull-pattern
+  [:block/uuid
+   {:block/left [:block/uuid]}
+   {:block/parent [:block/uuid]}
+   :block/content
+   :block/created-at
+   :block/updated-at
+   :block/format
+   {:block/tags [:block/uuid]}])
+
+(defn- ->block-entity-map
+  [db eid]
+  (let [m (-> (d/pull db entity-map-pull-pattern eid)
+              (update :block/left :block/uuid)
+              (update :block/parent :block/uuid))]
+    (if (seq (:block/tags m))
+      (update m :block/tags (partial mapv :block/uuid))
+      m)))
+
+(defn- normal-block?
+  [entity]
+  (and (:block/parent entity)
+       (:block/left entity)))
+
+
+(defn- entity-datoms=>ops
+  [db-before db-after id->attr->datom entity-datoms]
+  (when-let [e (ffirst entity-datoms)]
+    (let [attr->datom (id->attr->datom e)]
+      (when (seq attr->datom)
+        (let [{[_ _ block-uuid _ add1?]    :block/uuid
+               [_ _ block-content _ add2?] :block/content
+               [_ _ _ _ add3?]             :block/left
+               [_ _ _ _ add4?]             :block/parent} attr->datom
+              entity-before (d/entity db-before e)
+              entity-after (d/entity db-after e)]
+          (cond
+            (and (not add1?) block-uuid
+                 (normal-block? entity-before))
+            [[:remove-block
+              {:block-uuid (:block/uuid entity-before)
+               :block-entity-map (->block-entity-map db-before e)}]]
+
+            (and add1? block-uuid
+                 (normal-block? entity-after))
+            [[:insert-block {:block-uuid (:block/uuid entity-after)}]]
+
+            (and (or add3? add4?)
+                 (normal-block? entity-after))
+            (cond-> [[:move-block
+                      {:block-uuid (:block/uuid entity-after)
+                       :block-origin-left (:block/uuid (:block/left entity-before))
+                       :block-origin-parent (:block/uuid (:block/parent entity-before))}]]
+              (and add2? block-content)
+              (conj [:update-block
+                     {:block-uuid (:block/uuid entity-after)
+                      :block-origin-content (:block/content entity-before)}]))
+
+            (and add2? block-content
+                 (normal-block? entity-after))
+            [[:update-block
+              {:block-uuid (:block/uuid entity-after)
+               :block-origin-content (:block/content entity-before)}]]))))))
+
+(defn- generate-undo-ops
+  [repo db-before db-after same-entity-datoms-coll id->attr->datom]
+  (let [ops (mapcat (partial entity-datoms=>ops db-before db-after id->attr->datom) same-entity-datoms-coll)]
+    (assert (undo-ops-validator ops) ops)
+    (when (seq ops)
+      (push-undo-ops repo ops))))
+
+
+(defmethod db-listener/listen-db-changes :gen-undo-ops
+  [_ {:keys [_tx-data tx-meta db-before db-after
+             repo id->attr->datom same-entity-datoms-coll]}]
+  (when (:gen-undo-op? tx-meta true)
+    (generate-undo-ops repo db-before db-after same-entity-datoms-coll id->attr->datom)))
+
+;;; listen db changes and push undo-ops (ends)

+ 10 - 10
src/resources/dicts/it.edn

@@ -585,11 +585,11 @@
  :plugin.install-from-file/title "Installa plugin da plugins.edn"
  :query/config-property-settings "Impostazioni per le proprietà di questa richiesta:"
  :right-side-bar/flashcards "Flashcard"
- :right-side-bar/history "(Dev) cronologia disfai/rifai"
+ :right-side-bar/history "(Dev) cronologia disfare/rifare"
  :right-side-bar/history-global "globale"
  :right-side-bar/history-pageonly "solo pagina corrente"
  :right-side-bar/history-redos "Rifai"
- :right-side-bar/history-undos "Disfai"
+ :right-side-bar/history-undos "Rifare"
  :right-side-bar/pane-close "Chiudi"
  :right-side-bar/pane-close-all "Chiudi tutti"
  :right-side-bar/pane-close-others "Chiudi altri"
@@ -649,9 +649,9 @@
  :settings-page/tab-editor "Editor"
  :settings-page/tab-features "Funzionalità"
  :settings-page/tab-keymap "Scorciatoie"
- :settings-page/theme-dark "scuro"
- :settings-page/theme-light "chiaro"
- :settings-page/theme-system "sistema"
+ :settings-page/theme-dark "Scuro"
+ :settings-page/theme-light "Chiaro"
+ :settings-page/theme-system "Sistema"
  :settings-page/update-available "Trovato una nuova versione "
  :settings-page/update-error-1 "⚠️ Ops, qualcosa è andato storto!"
  :settings-page/update-error-2 " Per favore, controlla il "
@@ -703,8 +703,8 @@
  :whiteboard/link-to-any-page-or-block "Collega a qualsiasi pagina o blocco"
  :whiteboard/lock "Blocca"
  :whiteboard/medium "Medio"
- :whiteboard/move-to-back "Sposta in retro"
- :whiteboard/move-to-front "Sposta in fronte"
+ :whiteboard/move-to-back "Sposta sul retro"
+ :whiteboard/move-to-front "Sposta sul fronte"
  :whiteboard/new-block "Nuovo blocco:"
  :whiteboard/new-block-no-colon "Nuovo blocco"
  :whiteboard/new-page "Nuova pagina:"
@@ -713,8 +713,8 @@
  :whiteboard/open-page "Apri pagina"
  :whiteboard/open-page-in-sidebar "Apri pagina nel pannello laterale"
  :whiteboard/open-twitter-url "Apri link Twitter"
- :whiteboard/open-website-url "Open sito internet"
- :whiteboard/open-youtube-url "Open link YouTube"
+ :whiteboard/open-website-url "Apri sito internet"
+ :whiteboard/open-youtube-url "Apri link YouTube"
  :whiteboard/pack-into-rectangle "Forma rettangolo compresso"
  :whiteboard/pan "Muovi"
  :whiteboard/paste "Incolla"
@@ -741,7 +741,7 @@
  :whiteboard/toggle-pen-mode "Attiva/disattiva modalità penna"
  :whiteboard/triangle "Triangolo"
  :whiteboard/twitter-url "Indirizzo Twitter"
- :whiteboard/undo "Disfai"
+ :whiteboard/undo "Rifare"
  :whiteboard/ungroup "Annulla raggruppamento"
  :whiteboard/unlock "Blocca"
  :whiteboard/website-url "Link sito internet"

+ 7 - 4
src/test/frontend/util_test.cljs

@@ -15,14 +15,17 @@
     (is (= 0 (util/safe-dec-current-pos-from-end "😀" 2)))
     (is (= 0 (util/safe-dec-current-pos-from-end "a" 1)))
     (is (= 4 (util/safe-dec-current-pos-from-end "abcde" 5)))
-    (is (= 1 (util/safe-dec-current-pos-from-end "中文" 2))))
+    (is (= 1 (util/safe-dec-current-pos-from-end "中文" 2)))
+    (is (= 0 (util/safe-dec-current-pos-from-end "中" 1)))
+    (is (= 0 (util/safe-dec-current-pos-from-end "a" 1))))
 
   (testing "safe current position from start for emoji"
     (is (= 5 (util/safe-inc-current-pos-from-start "abc😀d" 3)))
-    (is (= 2 (util/safe-inc-current-pos-from-start "😀" 0)))
     (is (= 2 (util/safe-inc-current-pos-from-start "abcde" 1)))
-    (is (= 1 (util/safe-inc-current-pos-from-start "a" 0)))
-    (is (= 1 (util/safe-inc-current-pos-from-start "中文" 0)))))
+    (is (= 1 (util/safe-inc-current-pos-from-start "中文" 0)))
+    (is (= 2 (util/safe-inc-current-pos-from-start "😀" 0)))
+    (is (= 1 (util/safe-inc-current-pos-from-start "中" 0)))
+    (is (= 1 (util/safe-inc-current-pos-from-start "a" 0)))))
 
 (deftest test-get-line-pos
   (testing "get-line-pos"

+ 2 - 0
src/test/frontend/worker/undo_redo_test.cljs

@@ -8,4 +8,6 @@
   ;; TODO: add tests for undo-redo
   undo-redo/undo-op-schema
   undo-redo/reverse-op
+  undo-redo/undo
+  undo-redo/redo
   )

+ 4 - 4
static/yarn.lock

@@ -1856,10 +1856,10 @@ ds-store@^0.1.5:
     macos-alias "~0.2.5"
     tn1150 "^0.1.0"
 
[email protected].0:
-  version "2.5.0"
-  resolved "https://registry.yarnpkg.com/dugite/-/dugite-2.5.0.tgz#8b235564fdf8692688283c714149a59d9da79865"
-  integrity sha512-sYsSOqV7NidthDtMUPgKCvqMGqswKkcyAxOMhwEswlcGZ+kHadT2SEDFUJOy0AVR/yTJL6wBF7q1OiySfU0gGA==
[email protected].1:
+  version "2.5.1"
+  resolved "https://registry.yarnpkg.com/dugite/-/dugite-2.5.1.tgz#6ab808ebf321809edf42d974e62eea9c9e256722"
+  integrity sha512-9OjUguynzq8v3GSmp01kbVcMmErc65ZZ0OssO/0PM2RyhD8Dzb8cCuy3z72+IxLwPwNi754jZ0FtMLAFA3D0qA==
   dependencies:
     progress "^2.0.3"
     tar "^6.1.11"