From 43d483d3508889d176f60ac8ac7f6963edb73759 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 24 Dec 2025 18:13:59 +0800 Subject: [PATCH 01/10] fix: rtc e2e tests --- clj-e2e/src/logseq/e2e/graph.clj | 1 - 1 file changed, 1 deletion(-) diff --git a/clj-e2e/src/logseq/e2e/graph.clj b/clj-e2e/src/logseq/e2e/graph.clj index 62a4a22065..6c40f8bf3f 100644 --- a/clj-e2e/src/logseq/e2e/graph.clj +++ b/clj-e2e/src/logseq/e2e/graph.clj @@ -31,7 +31,6 @@ (when enable-sync? (w/wait-for "button#rtc-sync" {:timeout 3000}) (w/click "button#rtc-sync")) - (w/click "button:not([disabled]):text(\"Submit\")") (when enable-sync? (input-e2ee-password) (w/wait-for "button.cloud.on.idle" {:timeout 20000})) From 82a1932e9bd5bbdc68e188b36cd43c778b9ca469 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 24 Dec 2025 20:31:25 +0800 Subject: [PATCH 02/10] fix: copy and pasting blocks with dates does not work correctly related to https://github.com/logseq/db-test/issues/645 --- deps/outliner/src/logseq/outliner/core.cljs | 5 +++-- src/main/frontend/handler/editor.cljs | 4 +++- src/main/frontend/handler/paste.cljs | 3 ++- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/deps/outliner/src/logseq/outliner/core.cljs b/deps/outliner/src/logseq/outliner/core.cljs index 6fcc188fc6..8df606dfad 100644 --- a/deps/outliner/src/logseq/outliner/core.cljs +++ b/deps/outliner/src/logseq/outliner/core.cljs @@ -791,7 +791,7 @@ `update-timestamps?`: whether to update `blocks` timestamps. ``" [repo db blocks target-block {:keys [_sibling? keep-uuid? keep-block-order? - outliner-op replace-empty-target? update-timestamps? + outliner-op outliner-real-op replace-empty-target? update-timestamps? insert-template?] :as opts :or {update-timestamps? true}}] @@ -810,7 +810,8 @@ b) b) dissoc-keys (concat [:block/tx-id] - (when (contains? #{:insert-template-blocks :paste} outliner-op) + (when (and (contains? #{:insert-template-blocks :paste} outliner-op) + (not (contains? #{:paste-text} outliner-real-op))) [:block/refs]))] (apply dissoc b' dissoc-keys)) b)) diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index 3d6102f501..b68ea534ca 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -2131,7 +2131,8 @@ keep-uuid? revert-cut-txs skip-empty-target? - ops-only?] + ops-only? + outliner-real-op] :or {exclude-properties []}}] (let [editing-block (when-let [editing-block (state/get-edit-block)] (some-> (db/entity [:block/uuid (:block/uuid editing-block)]) @@ -2177,6 +2178,7 @@ blocks)] (outliner-op/insert-blocks! blocks' target-block' {:sibling? sibling? :outliner-op :paste + :outliner-real-op outliner-real-op :replace-empty-target? replace-empty-target? :keep-uuid? keep-uuid?}))))] (if ops-only? diff --git a/src/main/frontend/handler/paste.cljs b/src/main/frontend/handler/paste.cljs index e599a9ab4e..817d0fefec 100644 --- a/src/main/frontend/handler/paste.cljs +++ b/src/main/frontend/handler/paste.cljs @@ -40,7 +40,8 @@ (update :block/title (fn [title] (let [title' (db-content/replace-tags-with-id-refs title refs)] (db-content/title-ref->id-ref title' refs)))))))))] - (editor-handler/paste-blocks blocks' {:keep-uuid? true})))) + (editor-handler/paste-blocks blocks' {:keep-uuid? true + :outliner-real-op :paste-text})))) (defn- paste-segmented-text [format text] From c167b6954083936d9020055d65785f5fcc8612c2 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 24 Dec 2025 20:37:47 +0800 Subject: [PATCH 03/10] enhance: select match --- src/main/frontend/components/select.cljs | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/components/select.cljs b/src/main/frontend/components/select.cljs index 19ac7b3c54..9222093afd 100644 --- a/src/main/frontend/components/select.cljs +++ b/src/main/frontend/components/select.cljs @@ -144,9 +144,23 @@ (not exact-match?) (not (string/blank? @*input)) (not (exact-match-exclude-items @*input))) - (->> - (cons new-option search-result') - (remove nil?)) + (let [current-input (exact-transform-fn @*input) + matches? (some (fn [item] + (and (string? item) + (string? current-input) + (string/includes? + (string/lower-case item) + (string/lower-case current-input)))) + (set (map (comp exact-transform-fn str extract-fn) search-result')))] + (->> + (if matches? + (cons + (first search-result') + (cons + new-option + (rest search-result'))) + (cons new-option search-result')) + (remove nil?))) search-result') input-opts' (if (fn? input-opts) (input-opts (empty? search-result)) input-opts) input-container (or From e29cb6d5a0d9a114c36907dcde325ec0936191c4 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 24 Dec 2025 20:50:18 +0800 Subject: [PATCH 04/10] fix: page reference doesn't recoginize pasted text fix https://github.com/logseq/db-test/issues/671 --- src/main/frontend/components/editor.cljs | 26 +++++++++++++++--------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/main/frontend/components/editor.cljs b/src/main/frontend/components/editor.cljs index e55f52090b..f938c2b354 100644 --- a/src/main/frontend/components/editor.cljs +++ b/src/main/frontend/components/editor.cljs @@ -255,18 +255,20 @@ [:code (if util/mac? "Cmd+Enter" "Ctrl+Enter")] [:span " to display this tag inline instead of at the end of this node."]])]))) -(rum/defc page-search < rum/reactive - {:will-unmount (fn [state] +(rum/defcs page-search < rum/reactive + {:init (fn [state] + (assoc state ::pos (state/get-editor-last-pos))) + :will-unmount (fn [state] (reset! commands/*current-command nil) state)} "Page or tag searching popup" - [id format] + [state id format] (let [action (state/sub :editor/action) db? (config/db-based-graph? (state/get-current-repo)) embed? (and db? (= @commands/*current-command "Page embed")) tag? (= action :page-search-hashtag) db-tag? (and db? tag?) - pos (state/get-editor-last-pos) + pos (::pos state) input (gdom/getElement id)] (when input (let [current-pos (cursor/pos input) @@ -382,9 +384,11 @@ (:block/title template)) :class "black"}))) -(rum/defc template-search < rum/reactive - [id _format] - (let [pos (state/get-editor-last-pos) +(rum/defcs template-search < rum/reactive + {:init (fn [state] + (assoc state ::pos (state/get-editor-last-pos)))} + [state id _format] + (let [pos (::pos state) input (gdom/getElement id)] (when input (let [current-pos (cursor/pos input) @@ -467,11 +471,13 @@ [last-pos current-pos]) [:<>]) -(rum/defc code-block-mode-picker < rum/reactive - [id format] +(rum/defcs code-block-mode-picker < rum/reactive + {:init (fn [state] + (assoc state ::pos (state/get-editor-last-pos)))} + [state id format] (when-let [modes (some->> js/window.CodeMirror (.-modes) (js/Object.keys) (js->clj) (remove #(= "null" %)))] (when-let [^js input (gdom/getElement id)] - (let [pos (state/get-editor-last-pos) + (let [pos (::pos state) current-pos (cursor/pos input) edit-content (or (state/sub-edit-content) "") q (or (editor-handler/get-selected-text) From a97fec165ff3789361500f1190604f9ecd870a31 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 24 Dec 2025 21:09:10 +0800 Subject: [PATCH 05/10] fix: can't copy PDF highlights refs fix https://github.com/logseq/db-test/issues/673 fix https://github.com/logseq/db-test/issues/650 --- src/main/frontend/extensions/pdf/assets.cljs | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/main/frontend/extensions/pdf/assets.cljs b/src/main/frontend/extensions/pdf/assets.cljs index fbb51d1c2c..f03c033a71 100644 --- a/src/main/frontend/extensions/pdf/assets.cljs +++ b/src/main/frontend/extensions/pdf/assets.cljs @@ -188,14 +188,13 @@ (defn ensure-ref-block! [pdf-current hl insert-opts] (if (config/db-based-graph? (state/get-current-repo)) - (p/chain - (db-based-ensure-ref-block! pdf-current hl insert-opts) - (fn [] - ;; try to move the asset block to the ref block - (let [ref-block (db-model/query-block-by-uuid (:id hl)) - asset-block (:logseq.property.pdf/hl-image ref-block)] - (when asset-block - (editor-handler/move-blocks! [asset-block] ref-block {:sibling? false}))))) + (p/let [ref-block (db-based-ensure-ref-block! pdf-current hl insert-opts) + asset-block (:logseq.property.pdf/hl-image ref-block)] + ;; try to move the asset block to the ref block + (p/do! + (when asset-block + (editor-handler/move-blocks! [asset-block] ref-block {:sibling? false})) + ref-block)) (file-based-ensure-ref-block! pdf-current hl insert-opts))) (defn file-based-load-hls-data$ From b2497c0cc571aece2a6bcd4697b5c50c40a3e7b3 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 24 Dec 2025 22:30:10 +0800 Subject: [PATCH 06/10] fix: pages starting with "@" fail to import from markdown related to https://github.com/logseq/db-test/issues/680 --- .../src/logseq/graph_parser/exporter.cljs | 34 +++++++++++-------- src/main/frontend/components/imports.cljs | 2 +- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/deps/graph-parser/src/logseq/graph_parser/exporter.cljs b/deps/graph-parser/src/logseq/graph_parser/exporter.cljs index cbdbebf6a2..b032c38fbf 100644 --- a/deps/graph-parser/src/logseq/graph_parser/exporter.cljs +++ b/deps/graph-parser/src/logseq/graph_parser/exporter.cljs @@ -892,14 +892,15 @@ ;; {:url ["Complex" {:protocol "zotero", :link "select/library/items/6VCW9QFJ"}], :label [["Plain" "Dechow and Struppa - 2015 - Intertwingled.pdf"]], :full_text "[Dechow and Struppa - 2015 - Intertwingled.pdf](zotero://select/library/items/6VCW9QFJ)", :metadata ""} (defn- get-zotero-local-pdf-path [config m] - (let [link (:link (second (:url m))) - label (second (first (:label m))) - id (last (string/split link #"/"))] - (when (and link id label) - (when-let [zotero-data-dir (get-in config [:zotero/settings-v2 "default" :zotero-data-directory])] - {:link (str "zotero://" link) - :path (node-path/join zotero-data-dir "storage" id label) - :base label})))) + (when (= "zotero" (:protocol (second (:url m)))) + (let [link (:link (second (:url m))) + label (second (first (:label m))) + id (last (string/split link #"/"))] + (when (and link id label) + (when-let [zotero-data-dir (get-in config [:zotero/settings-v2 "default" :zotero-data-directory])] + {:link (str "zotero://" link) + :path (node-path/join zotero-data-dir "storage" id label) + :base label}))))) (defn- walk-ast-blocks "Walks each ast block in order to its full depth. Saves multiple ast types for @@ -1048,10 +1049,11 @@ "Given an asset's relative or full path, create a unique name for identifying an asset. Must handle to paths as ../assets/*, assets/* and with subdirectories" [path] - (or (re-find #"assets/.*$" path) - ;; pdf outside logseq graphs - (when (string/ends-with? path ".pdf") - path))) + (when (string? path) + (or (re-find #"assets/.*$" path) + ;; pdf outside logseq graphs + (when (string/ends-with? path ".pdf") + path)))) (defn- update-asset-links-in-block-title [block-title asset-name-to-uuids ignored-assets] (reduce (fn [acc [asset-name asset-uuid]] @@ -1213,11 +1215,13 @@ (p/let [asset-maps* (p/all (map (fn [asset-link] (p/let [path* (-> asset-link second :url second) + zotero-asset? (when (map? path*) + (= "zotero" (:protocol (second (:url (second asset-link)))))) {:keys [path link base]} (if (map? path*) (get-zotero-local-pdf-path user-config (second asset-link)) {:path path*}) - asset-name (-> path asset-path->name) - asset-link-or-name (or link (-> path asset-path->name)) + asset-name (some-> path asset-path->name) + asset-link-or-name (or link (some-> path asset-path->name)) asset-data* (when asset-link-or-name (get @assets asset-link-or-name)) _ (when (and asset-link-or-name (not asset-data*) @@ -1261,7 +1265,7 @@ (swap! assets assoc-in [asset-link-or-name :asset-created?] true) {:asset-name-uuid [asset-link-or-name (:block/uuid new-asset)] :asset-tx asset-tx})) - (do + (when-not zotero-asset? ; no need to report warning for zotero managed pdf files (swap! ignored-assets conj {:reason "No asset data found for this asset path" :path (-> asset-link second :url second) diff --git a/src/main/frontend/components/imports.cljs b/src/main/frontend/components/imports.cljs index 73be0f9437..7b031fa4e0 100644 --- a/src/main/frontend/components/imports.cljs +++ b/src/main/frontend/components/imports.cljs @@ -363,7 +363,7 @@ bytes-array (js/Uint8Array. buffer) checksum (db-asset/name (:path file)) + asset-name (some-> (:path file) gp-exporter/asset-path->name) assets-dir (path/path-join repo-dir common-config/local-assets-dir) asset-type (db-asset/asset-path->type (:path file)) {:keys [with-edn-content pdf-annotation?]} (buffer-handler bytes-array)] From afd4210906b240e24bf06faa6606bf07a67b4b2d Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Wed, 24 Dec 2025 22:56:47 +0800 Subject: [PATCH 07/10] fix: auto backup --- src/main/frontend/handler/events.cljs | 23 ++++++++++++----------- src/main/frontend/handler/export.cljs | 2 +- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index 7796ace397..a2e20616e5 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -178,14 +178,12 @@ (not util/nfs?)) (state/pub-event! [:graph/dir-gone dir])))) (let [db-based? (config/db-based-graph? repo)] - (p/do! - (state/pub-event! [:graph/sync-context]) - ;; FIXME: an ugly implementation for redirecting to page on new window is restored - (repo-handler/graph-ready! repo) - (when-not config/publishing? - (if db-based? - (export/auto-db-backup! repo {:backup-now? true}) - (fs-watcher/load-graph-files! repo)))))) + ;; FIXME: an ugly implementation for redirecting to page on new window is restored + (repo-handler/graph-ready! repo) + + (when-not config/publishing? + (when-not db-based? + (fs-watcher/load-graph-files! repo))))) (defmethod handle :instrument [[_ {:keys [type payload] :as opts}]] (when-not (empty? (dissoc opts :type :payload)) @@ -196,8 +194,8 @@ (let [[user-uuid graph-uuid tx-id] @sync/graphs-txid payload (merge {:schema-version (str db-schema/version) - :db-schema-version (when-let [db (frontend.db/get-db)] - (str (:kv/value (frontend.db/entity db :logseq.kv/schema-version)))) + :db-schema-version (when-let [db (db/get-db)] + (str (:kv/value (db/entity db :logseq.kv/schema-version)))) :user-id user-uuid :graph-id graph-uuid :tx-id tx-id @@ -260,9 +258,12 @@ (defmethod handle :graph/restored [[_ graph]] (when graph (assets-handler/ensure-assets-dir! graph)) + (state/pub-event! [:graph/sync-context]) + (when (config/db-based-graph? graph) + (export/auto-db-backup! graph {:backup-now? true})) (rtc-flows/trigger-rtc-start graph) (fsrs/update-due-cards-count) - (when-not (mobile-util/native-ios?) + (when-not (mobile-util/native-platform?) (state/pub-event! [:graph/ready graph]))) (defmethod handle :whiteboard-link [[_ shapes]] diff --git a/src/main/frontend/handler/export.cljs b/src/main/frontend/handler/export.cljs index 2693ac5483..768b23cc72 100644 --- a/src/main/frontend/handler/export.cljs +++ b/src/main/frontend/handler/export.cljs @@ -320,7 +320,7 @@ [repo {:keys [backup-now?] :or {backup-now? true}}] (when (ldb/get-key-value (db/get-db repo) :logseq.kv/graph-backup-folder) - (when (and (config/db-based-graph? repo) util/web-platform? (utils/nfsSupported)) + (when (and (config/db-based-graph? repo) (not (util/capacitor?))) (cancel-db-backup!) (when backup-now? (backup-db-graph repo :backup-now)) From cf80caebf62f4618475a4849412cf2d083687435 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Thu, 25 Dec 2025 10:40:59 +0800 Subject: [PATCH 08/10] feat: db auto backup on desktop (#12275) * feat: db auto backup on desktop * fix: press delete closes right sidebar fix https://github.com/logseq/db-test/issues/670 * disable git backup for db graphs since it's confusing Users can still use external Git for version control. --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- deps/db/src/logseq/db/common/sqlite.cljs | 9 +- src/electron/electron/backup_file.cljs | 162 +++++++++++++++++---- src/electron/electron/db.cljs | 20 ++- src/electron/electron/git.cljs | 49 ++++--- src/main/frontend/components/export.cljs | 8 +- src/main/frontend/components/settings.cljs | 5 +- src/main/frontend/handler/events.cljs | 35 +++-- src/main/frontend/handler/export.cljs | 31 ++-- src/main/frontend/persist_db.cljs | 2 +- src/main/frontend/state.cljs | 9 ++ 10 files changed, 237 insertions(+), 93 deletions(-) diff --git a/deps/db/src/logseq/db/common/sqlite.cljs b/deps/db/src/logseq/db/common/sqlite.cljs index 79ae3f0c7d..b888bcf91d 100644 --- a/deps/db/src/logseq/db/common/sqlite.cljs +++ b/deps/db/src/logseq/db/common/sqlite.cljs @@ -4,8 +4,8 @@ (:require ["path" :as node-path] [clojure.string :as string] [datascript.core :as d] - [logseq.db.sqlite.util :as sqlite-util] - [logseq.common.config :as common-config])) + [logseq.common.config :as common-config] + [logseq.db.sqlite.util :as sqlite-util])) (defn create-kvs-table! "Creates a sqlite table for use with datascript.storage if one doesn't exist" @@ -40,3 +40,8 @@ (let [db-name' (sanitize-db-name db-name) graph-dir (node-path/join graphs-dir db-name')] [db-name' (node-path/join graph-dir "db.sqlite")])) + +(defn get-db-backups-path + [graphs-dir db-name] + (let [db-name' (sanitize-db-name db-name)] + (node-path/join graphs-dir db-name' "backups"))) diff --git a/src/electron/electron/backup_file.cljs b/src/electron/electron/backup_file.cljs index c4fb090e97..1d5adfe2e5 100644 --- a/src/electron/electron/backup_file.cljs +++ b/src/electron/electron/backup_file.cljs @@ -1,8 +1,8 @@ (ns electron.backup-file - (:require [clojure.string :as string] + (:require ["fs" :as fs] + ["fs-extra" :as fs-extra] ["path" :as node-path] - ["fs" :as fs] - ["fs-extra" :as fs-extra])) + [clojure.string :as string])) (def backup-dir "logseq/bak") (def version-file-dir "logseq/version-files/local") @@ -24,38 +24,142 @@ [repo relative-path] (get-backup-dir* repo relative-path version-file-dir)) -;; TODO: add interval support like days (defn- truncate-old-versioned-files! - "reserve the latest 6 version files" - [dir] - (let [files (fs/readdirSync dir (clj->js {:withFileTypes true})) - files (mapv #(.-name %) files) - old-versioned-files (drop 6 (reverse (sort files)))] + "reserve the latest `keep-versions` version files" + [dir keep-versions] + (let [entries (fs/readdirSync dir (clj->js {:withFileTypes true})) + files (->> entries + (filter #(.-isFile %)) + (mapv #(.-name %))) + old-versioned-files (drop keep-versions (reverse (sort files)))] (doseq [file old-versioned-files] (fs-extra/removeSync (node-path/join dir file))))) +(defn- parse-backup-ts + "Backup filenames are like: 2025-12-25T01_23_45.678Z.ext + We turn '_' back into ':' and parse as ISO." + [filename] + (let [base (-> filename + ;; drop extension (keep last '.' part) + (string/replace #"\.[^.]+$" "") + (string/replace "_" ":")) + ms (.parse js/Date base)] + (when-not (js/isNaN ms) ms))) + +(defn- truncate-daily-versioned-files! + "Keep the latest `keep-versions` version files, but: + - the newest 6 kept are deduped per-hour (keep newest file per hour) + - the remaining kept (if any) are deduped per-day (keep newest file per day) + + Example: keep-versions=12 => 6 hourly + 6 daily." + [dir keep-versions] + (let [keep-versions (max 0 (or keep-versions 0)) + keep-hourly (min 6 keep-versions) + + ;; list file names (ignore directories) + dirents (fs/readdirSync dir (clj->js {:withFileTypes true})) + files (->> dirents + (filter #(.-isFile %)) + (mapv #(.-name %))) + + ;; sort newest -> oldest primarily by parsed timestamp; fall back to name + files* (->> files + (map (fn [n] {:name n :ts (or (parse-backup-ts n) -1)})) + (sort-by (juxt (comp - :ts) :name)) + (mapv :name)) + + ;; decide which files to keep + keep-set + (loop [xs files* + kept #{} + kept-count 0 + hour-seen #{} + day-seen #{}] + (if (or (empty? xs) (>= kept-count keep-versions)) + kept + (let [f (first xs) + ts (parse-backup-ts f) + ;; derive keys; if unparsable, treat as unique bucket + hour-key (if ts + (.toISOString (js/Date. (-> ts + (js/Math.floor) + (- (mod ts 3600000))))) + (str "unparsable-hour:" f)) + day-key (if ts + (.slice (.toISOString (js/Date. ts)) 0 10) + (str "unparsable-day:" f))] + (cond + ;; Phase 1: hourly buckets (newest 6 hours) + (< (count hour-seen) keep-hourly) + (if (contains? hour-seen hour-key) + (recur (rest xs) kept kept-count hour-seen day-seen) + (recur (rest xs) + (conj kept f) + (inc kept-count) + (conj hour-seen hour-key) + day-seen)) + + ;; Phase 2: daily buckets (fill remaining up to keep-versions) + :else + (if (contains? day-seen day-key) + (recur (rest xs) kept kept-count hour-seen day-seen) + (recur (rest xs) + (conj kept f) + (inc kept-count) + hour-seen + (conj day-seen day-key))))))) + + ;; remove everything not in keep-set + to-remove (remove keep-set files)] + (doseq [file to-remove] + (fs-extra/removeSync (node-path/join dir file))))) + +(defn- latest-backup-info + "Return {:name .. :ts .. :size ..} for the latest backup in dir, or nil. + Prefers timestamp parsed from filename; falls back to file mtimeMs." + [dir] + (let [dirents (fs/readdirSync dir (clj->js {:withFileTypes true})) + files (->> dirents (filter #(.-isFile %)) (map #(.-name %)))] + (when (seq files) + (->> files + (map (fn [name] + (let [p (node-path/join dir name) + stat (fs/statSync p) + ts (or (parse-backup-ts name) (.-mtimeMs stat))] + {:name name + :ts ts + :size (.-size stat)}))) + (apply max-key :ts))))) + +(defn- too-soon? + [dir] + (let [info (latest-backup-info dir) + ;; default: if using daily+hourly retention, don’t create more than 1 per hour + min-interval-ms 3600000 + now-ms (.now js/Date) + latest-backup-ts (:ts info)] + (and latest-backup-ts + (pos? min-interval-ms) + (< (- now-ms latest-backup-ts) min-interval-ms)))) + (defn backup-file "backup CONTENT under DIR :backup-dir or :version-file-dir :backup-dir = `backup-dir` :version-file-dir = `version-file-dir`" - [repo dir relative-path ext content & {:keys [add-desktop? skip-backup-fn] - :or {add-desktop? true}}] - {:pre [(contains? #{:backup-dir :version-file-dir} dir)]} - (let [dir* (case dir - :backup-dir (get-backup-dir repo relative-path) - :version-file-dir (get-version-file-dir repo relative-path)) + [repo dir relative-path ext content & {:keys [truncate-daily? + keep-versions backups-dir] + :or {keep-versions 6}}] + (let [dir* (or backups-dir + (case dir + :backup-dir (get-backup-dir repo relative-path) + :version-file-dir (get-version-file-dir repo relative-path))) _ (fs-extra/ensureDirSync dir*) - backups (fs/readdirSync dir*) - latest-backup-size (when (seq backups) - (some->> (nth backups (dec (count backups))) - (node-path/join dir*) - (fs/statSync) - (.-size)))] - (when-not (and (fn? skip-backup-fn) latest-backup-size (skip-backup-fn latest-backup-size)) - (let [new-path (node-path/join dir* - (str (string/replace (.toISOString (js/Date.)) ":" "_") - (when add-desktop? ".Desktop") - ext))] - (fs/writeFileSync new-path content) - (fs/statSync new-path) - (truncate-old-versioned-files! dir*))))) + new-path (node-path/join dir* + (str (string/replace (.toISOString (js/Date.)) ":" "_") + ext))] + (when-not (and truncate-daily? (too-soon? dir*)) + (fs/writeFileSync new-path content) + (fs/statSync new-path) + (if truncate-daily? + (truncate-daily-versioned-files! dir* keep-versions) + (truncate-old-versioned-files! dir* keep-versions))))) diff --git a/src/electron/electron/db.cljs b/src/electron/electron/db.cljs index 27912ef83b..1208a14b05 100644 --- a/src/electron/electron/db.cljs +++ b/src/electron/electron/db.cljs @@ -2,6 +2,7 @@ "Provides SQLite dbs for electron and manages files of those dbs" (:require ["fs-extra" :as fs] ["path" :as node-path] + [electron.backup-file :as backup-file] [logseq.cli.common.graph :as cli-common-graph] [logseq.common.config :as common-config] [logseq.db.common.sqlite :as common-sqlite])) @@ -17,11 +18,6 @@ (fs/ensureDirSync graph-dir) graph-dir)) -(defn save-db! - [db-name data] - (let [[_db-name db-path] (common-sqlite/get-db-full-path (cli-common-graph/get-db-graphs-dir) db-name)] - (fs/writeFileSync db-path data))) - (defn get-db [db-name] (let [_ (ensure-graph-dir! db-name) @@ -29,6 +25,20 @@ (when (fs/existsSync db-path) (fs/readFileSync db-path)))) +(defn save-db! + [db-name data] + (let [[db-name db-path] (common-sqlite/get-db-full-path (cli-common-graph/get-db-graphs-dir) db-name) + old-data (get-db db-name) + backups-path (common-sqlite/get-db-backups-path (cli-common-graph/get-db-graphs-dir) db-name)] + (when old-data + (backup-file/backup-file db-name nil nil + ".sqlite" + old-data + {:backups-dir backups-path + :truncate-daily? true + :keep-versions 12})) + (fs/writeFileSync db-path data))) + (defn unlink-graph! [repo] (let [db-name (common-sqlite/sanitize-db-name repo) diff --git a/src/electron/electron/git.cljs b/src/electron/electron/git.cljs index b4582b2cdd..24387e74a0 100644 --- a/src/electron/electron/git.cljs +++ b/src/electron/electron/git.cljs @@ -1,14 +1,14 @@ (ns electron.git (:require ["dugite" :refer [GitProcess]] - [goog.object :as gobj] + ["fs-extra" :as fs] + ["os" :as os] + ["path" :as node-path] + [clojure.string :as string] + [electron.logger :as logger] [electron.state :as state] [electron.utils :as utils] - [electron.logger :as logger] - [promesa.core :as p] - [clojure.string :as string] - ["fs-extra" :as fs] - ["path" :as node-path] - ["os" :as os])) + [goog.object :as gobj] + [promesa.core :as p])) (def log-error (partial logger/error "[Git]")) @@ -111,27 +111,30 @@ (defn add-all-and-commit-single-graph! [graph-path message] - (let [message (if (string/blank? message) - "Auto saved by Logseq" - message)] - (-> - (p/let [_ (init! graph-path) - _ (add-all! graph-path)] - (commit! graph-path message)) - (p/catch (fn [error] - (when (and - (string? error) - (not (string/blank? error))) - (if (string/starts-with? error "Author identity unknown") - (utils/send-to-renderer "setGitUsernameAndEmail" {:type "git"}) - (utils/send-to-renderer "notification" {:type "error" - :payload (str error "\nIf you don't want to see those errors or don't need git, you can disable the \"Git auto commit\" feature on Settings > Version control.")})))))))) + ;; Don't run git on db graphs + (when (string/includes? graph-path "logseq_local_") + (let [message (if (string/blank? message) + "Auto saved by Logseq" + message)] + (-> + (p/let [_ (init! graph-path) + _ (add-all! graph-path)] + (commit! graph-path message)) + (p/catch (fn [error] + (when (and + (string? error) + (not (string/blank? error))) + (if (string/starts-with? error "Author identity unknown") + (utils/send-to-renderer "setGitUsernameAndEmail" {:type "git"}) + (utils/send-to-renderer "notification" {:type "error" + :payload (str error "\nIf you don't want to see those errors or don't need git, you can disable the \"Git auto commit\" feature on Settings > Version control.")}))))))))) (defn add-all-and-commit! ([] (add-all-and-commit! nil)) ([message] - (doseq [path (state/get-all-graph-paths)] (add-all-and-commit-single-graph! path message)))) + (doseq [path (state/get-all-graph-paths)] + (add-all-and-commit-single-graph! path message)))) (defn short-status! [graph-path] diff --git a/src/main/frontend/components/export.cljs b/src/main/frontend/components/export.cljs index ec5973e9c2..62ce58ac8e 100644 --- a/src/main/frontend/components/export.cljs +++ b/src/main/frontend/components/export.cljs @@ -63,14 +63,14 @@ {:variant :default :on-click (fn [] (-> - (p/let [result (export/backup-db-graph repo :set-folder)] + (p/let [result (export/backup-db-graph repo)] (case result true (notification/show! "Backup successful!" :success) :graph-not-changed (notification/show! "Graph has not been updated since last export." :success) nil) - (export/auto-db-backup! repo {:backup-now? false})) + (export/auto-db-backup! repo)) (p/catch (fn [error] (println "Failed to backup.") (js/console.error error)))))} @@ -139,7 +139,9 @@ "Export debug transit file"] [:p.text-sm.opacity-70.mb-0 "Exports to a .transit file to send to us for debugging. Any sensitive data will be removed in the exported file."]]) - (when (and db-based? (not (util/mobile?))) + (when (and db-based? + util/web-platform? + (not (util/mobile?))) [:div [:hr] (auto-backup)])]]))) diff --git a/src/main/frontend/components/settings.cljs b/src/main/frontend/components/settings.cljs index 9a11da1dba..93ebd45295 100644 --- a/src/main/frontend/components/settings.cljs +++ b/src/main/frontend/components/settings.cljs @@ -801,7 +801,8 @@ (tooltip-row t enable-tooltip?)) (timetracking-row t enable-timetracking?) (enable-all-pages-public-row t enable-all-pages-public?) - (auto-push-row t current-repo enable-git-auto-push?)])) + (when-not db-graph? + (auto-push-row t current-repo enable-git-auto-push?))])) (rum/defc settings-git [] @@ -1505,7 +1506,7 @@ (when db-based? [:ai (t :settings-page/tab-ai) (t :settings-page/ai) (ui/icon "wand")]) - (when (util/electron?) + (when (and (util/electron?) (not db-based?)) [:version-control "git" (t :settings-page/tab-version-control) (ui/icon "history")]) ;; (when (util/electron?) diff --git a/src/main/frontend/handler/events.cljs b/src/main/frontend/handler/events.cljs index a2e20616e5..5b6f7a369c 100644 --- a/src/main/frontend/handler/events.cljs +++ b/src/main/frontend/handler/events.cljs @@ -113,19 +113,25 @@ (repo-handler/refresh-repos!)))) (defmethod handle :graph/switch [[_ graph opts]] - (export/cancel-db-backup!) - (persist-db/export-current-graph!) - (state/set-state! :db/async-queries {}) - (st/refresh!) - - (p/let [writes-finished? (state/tx repo) db (db/get-db repo)] (or (nil? tx) - (> tx (:max-tx db))))) + (> (:max-tx db) tx)))) (defn export-current-graph! [& {:keys [succ-notification? force-save?]}] diff --git a/src/main/frontend/state.cljs b/src/main/frontend/state.cljs index 76481cb3f1..893cb2baff 100644 --- a/src/main/frontend/state.cljs +++ b/src/main/frontend/state.cljs @@ -1301,12 +1301,21 @@ Similar to re-frame subscriptions" [item] (update-state! [:ui/navigation-item-collapsed? item] not)) +(declare sidebar-add-block!) +(defn- sidebar-add-content-when-open! + [] + (when (empty? (:sidebar/blocks @state)) + (sidebar-add-block! (get-current-repo) "contents" :contents))) + (defn toggle-sidebar-open?! [] + (when-not (:ui/sidebar-open? @state) + (sidebar-add-content-when-open!)) (swap! state update :ui/sidebar-open? not)) (defn open-right-sidebar! [] + (sidebar-add-content-when-open!) (swap! state assoc :ui/sidebar-open? true)) (defn hide-right-sidebar! From 9df740c7971107712a363e6128d28485c0673683 Mon Sep 17 00:00:00 2001 From: Tienson Qin Date: Thu, 25 Dec 2025 11:03:46 +0800 Subject: [PATCH 09/10] fix: multi-tabs e2e tests --- clj-e2e/src/logseq/e2e/graph.clj | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/clj-e2e/src/logseq/e2e/graph.clj b/clj-e2e/src/logseq/e2e/graph.clj index 6c40f8bf3f..407d6a9bd8 100644 --- a/clj-e2e/src/logseq/e2e/graph.clj +++ b/clj-e2e/src/logseq/e2e/graph.clj @@ -30,10 +30,11 @@ (util/input graph-name) (when enable-sync? (w/wait-for "button#rtc-sync" {:timeout 3000}) - (w/click "button#rtc-sync")) - (when enable-sync? + (w/click "button#rtc-sync") (input-e2ee-password) (w/wait-for "button.cloud.on.idle" {:timeout 20000})) + (when-not enable-sync? + (w/click "button:not([disabled]):text(\"Submit\")")) ;; new graph can blocks the ui because the db need to be created and restored, ;; I have no idea why `search-and-click` failed to auto-wait sometimes. (util/wait-timeout 1000)) From 33e34ab1be09c0e455933a6a654dd4477a4bfe60 Mon Sep 17 00:00:00 2001 From: "A. S." <12621257+Victor239@users.noreply.github.com> Date: Thu, 25 Dec 2025 03:32:57 +0000 Subject: [PATCH 10/10] Fix vertical line collapse behaviour (#12248) --- src/main/frontend/handler/editor.cljs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index b68ea534ca..ffd1ddf13d 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -3853,10 +3853,11 @@ (defn toggle-open-block-children! [block-id] (p/let [blocks (