mirror of
https://github.com/logseq/logseq.git
synced 2026-06-01 19:01:22 +00:00
Merge remote-tracking branch 'origin/master' into feat/cliable
This commit is contained in:
@@ -13,7 +13,7 @@
|
||||
[electron.server :as server]
|
||||
[electron.updater :refer [init-updater] :as updater]
|
||||
[electron.url :refer [logseq-url-handler]]
|
||||
[electron.utils :refer [*win mac? linux? dev? get-win-from-sender
|
||||
[electron.utils :refer [*win mac? dev? get-win-from-sender
|
||||
decode-protected-assets-schema-path send-to-renderer]
|
||||
:as utils]
|
||||
[electron.window :as win]
|
||||
@@ -33,14 +33,10 @@
|
||||
(defonce *teardown-fn (volatile! nil))
|
||||
(defonce *quit-dirty? (volatile! true))
|
||||
|
||||
;; Handle creating/removing shortcuts on Windows when installing/uninstalling.
|
||||
(when (js/require "electron-squirrel-startup") (.quit app))
|
||||
|
||||
(defn setup-updater! [^js win]
|
||||
;; manual/auto updater
|
||||
(when-not linux?
|
||||
(init-updater {:repo "logseq/logseq"
|
||||
:win win})))
|
||||
(init-updater {:repo "logseq/logseq"
|
||||
:win win}))
|
||||
|
||||
(defn open-url-handler
|
||||
"win - the main window instance (first renderer process)
|
||||
|
||||
@@ -5,7 +5,8 @@
|
||||
["abort-controller" :as AbortController]
|
||||
["buffer" :as buffer]
|
||||
["diff-match-patch" :as google-diff]
|
||||
["electron" :refer [app autoUpdater dialog ipcMain shell]]
|
||||
["electron" :refer [app dialog ipcMain shell]]
|
||||
["electron-updater" :refer [autoUpdater]]
|
||||
["electron-window-state" :as windowStateKeeper]
|
||||
["fs" :as fs]
|
||||
["fs-extra" :as fs-extra]
|
||||
@@ -421,7 +422,8 @@
|
||||
|
||||
(defmethod handle :quitAndInstall []
|
||||
(logger/info ::quick-and-install)
|
||||
(.quitAndInstall autoUpdater))
|
||||
;; https://www.electron.build/electron-updater.class.appupdater#quitandinstall
|
||||
(.quitAndInstall autoUpdater false true))
|
||||
|
||||
;; The graphHas* events are not used but maybe useful later?
|
||||
(defmethod handle :graphHasOtherWindow [^js win [_ graph]]
|
||||
|
||||
@@ -1,161 +1,163 @@
|
||||
(ns electron.updater
|
||||
(:require [electron.utils :refer [mac? win32? prod? open fetch *win]]
|
||||
[electron.logger :as logger]
|
||||
[frontend.version :refer [version]]
|
||||
[clojure.string :as string]
|
||||
[promesa.core :as p]
|
||||
[cljs-bean.core :as bean]
|
||||
(:require [cljs-bean.core :as bean]
|
||||
[electron.configs :as cfgs]
|
||||
["semver" :as semver]
|
||||
["os" :as os]
|
||||
["fs" :as fs]
|
||||
["path" :as node-path]
|
||||
["electron" :refer [ipcMain app autoUpdater]]))
|
||||
[electron.logger :as logger]
|
||||
[electron.utils :refer [*win prod?]]
|
||||
[frontend.version :refer [version]]
|
||||
["electron" :refer [ipcMain]]
|
||||
["electron-updater" :refer [autoUpdater]]))
|
||||
|
||||
(def *update-ready-to-install (atom nil))
|
||||
(def *update-pending (atom nil))
|
||||
(def *downloaded-update (atom nil))
|
||||
(def debug (partial logger/debug "[updater]"))
|
||||
(def electron-version version)
|
||||
|
||||
;Event: 'error'
|
||||
;Event: 'checking-for-update'
|
||||
;Event: 'update-available'
|
||||
;Event: 'update-not-available'
|
||||
;Event: 'download-progress'
|
||||
;Event: 'update-downloaded'
|
||||
;Event: 'completed'
|
||||
(defn- updater-channel
|
||||
[]
|
||||
(let [platform (.-platform js/process)
|
||||
arch (.-arch js/process)]
|
||||
(case platform
|
||||
"win32" (when (#{"x64" "arm64"} arch)
|
||||
(str "latest-" arch))
|
||||
"darwin" (when (#{"x64" "arm64"} arch)
|
||||
(str "latest-" arch))
|
||||
nil)))
|
||||
|
||||
(def electron-version
|
||||
(let [parts (string/split version #"\.")
|
||||
parts (take 3 parts)]
|
||||
(string/join "." parts)))
|
||||
(defn- emit-update!
|
||||
[^js win type payload]
|
||||
(when-let [web-contents (and win (. ^js win -webContents))]
|
||||
(.send web-contents "updates-callback"
|
||||
(bean/->js {:type type :payload payload}))))
|
||||
|
||||
(defn get-latest-artifact-info
|
||||
[repo]
|
||||
(let [endpoint (str "https://update.electronjs.org/" repo "/" js/process.platform "-" js/process.arch "/" electron-version)]
|
||||
(debug "checking" endpoint)
|
||||
(p/catch
|
||||
(p/let [res (fetch endpoint)
|
||||
status (.-status res)
|
||||
text (.text res)]
|
||||
(if (.-ok res)
|
||||
(let [info (when-not (string/blank? text) (js/JSON.parse text))]
|
||||
(bean/->clj info))
|
||||
(throw (js/Error. (str "[" status "] " text)))))
|
||||
(fn [e]
|
||||
(logger/warn "[update server error]" e)
|
||||
(throw e)))))
|
||||
(defn- emit-completed!
|
||||
[^js win]
|
||||
(emit-update! win "completed" nil))
|
||||
|
||||
(defn check-for-updates
|
||||
[{:keys [repo ^js win]
|
||||
[auto-download] :args}]
|
||||
(let [emit (fn [type payload]
|
||||
(.. win -webContents
|
||||
(send "updates-callback" (bean/->js {:type type :payload payload}))))]
|
||||
(debug "check for updates #" repo version)
|
||||
(p/create
|
||||
(fn [resolve reject]
|
||||
(emit "checking-for-update" nil)
|
||||
(-> (p/let
|
||||
[artifact (get-latest-artifact-info repo)
|
||||
(defn- normalize-payload
|
||||
[payload]
|
||||
(when payload
|
||||
(bean/->clj payload)))
|
||||
|
||||
artifact (when-let [remote-version (and artifact (re-find #"\d+\.\d+\.\d+" (:url artifact)))]
|
||||
(when (and (. semver valid remote-version)
|
||||
(. semver lt electron-version remote-version)) artifact))
|
||||
(defn- normalize-error
|
||||
[^js e]
|
||||
{:message (or (.-message e) (str e))})
|
||||
|
||||
url (if-not artifact (do (emit "update-not-available" nil) (throw (js/Error. "update not available"))) (:url artifact))
|
||||
_ (if url (emit "update-available" (bean/->js artifact)) (throw (js/Error. "download url not exists")))
|
||||
;; start download FIXME: user's preference about auto download
|
||||
_ (when-not auto-download (throw (js/Error. "no auto download")))
|
||||
^js dl-res (fetch url)
|
||||
_ (when-not (.-ok dl-res) (throw (js/Error. "download resource not available")))
|
||||
dest-info (p/create
|
||||
(fn [resolve1 reject1]
|
||||
(let [headers (. dl-res -headers)
|
||||
total-size (js/parseInt (.get headers "content-length"))
|
||||
body (.-body dl-res)
|
||||
start-at (.now js/Date)
|
||||
*downloaded (atom 0)
|
||||
dest-basename (node-path/basename url)
|
||||
tmp-dest-file (node-path/join (os/tmpdir) (str dest-basename ".pending"))
|
||||
dest-file (.createWriteStream fs tmp-dest-file)]
|
||||
(doto body
|
||||
(.on "data" (fn [chunk]
|
||||
(let [downloaded (+ @*downloaded (.-length chunk))
|
||||
percent (.toFixed (/ (* 100 downloaded) total-size) 2)
|
||||
elapsed (/ (- (js/Date.now) start-at) 1000)]
|
||||
(.write dest-file chunk)
|
||||
(emit "download-progress" {:total total-size
|
||||
:downloaded downloaded
|
||||
:percent percent
|
||||
:elapsed elapsed})
|
||||
(reset! *downloaded downloaded))))
|
||||
(.on "error" (fn [e]
|
||||
(reject1 e)))
|
||||
(.on "end" (fn [_e]
|
||||
(.close dest-file)
|
||||
(let [dest-file (string/replace tmp-dest-file ".pending" "")]
|
||||
(fs/renameSync tmp-dest-file dest-file)
|
||||
(resolve1 (merge artifact {:dest-file dest-file})))))))))]
|
||||
(reset! *update-ready-to-install dest-info)
|
||||
(emit "update-downloaded" dest-info)
|
||||
(resolve nil))
|
||||
(p/catch
|
||||
(fn [e]
|
||||
(if e
|
||||
(do
|
||||
(emit "error" e)
|
||||
(reject e))
|
||||
(resolve nil))))
|
||||
(p/finally
|
||||
(fn []
|
||||
(emit "completed" nil))))))))
|
||||
|
||||
(defn- new-version-downloaded-cb
|
||||
[_ notes name date url]
|
||||
(logger/info "[update-downloaded]" name notes date url)
|
||||
(defn- emit-update-downloaded!
|
||||
[payload]
|
||||
(when-let [web-contents (and @*win (. ^js @*win -webContents))]
|
||||
(.send web-contents "auto-updater-downloaded"
|
||||
(bean/->js {:notes notes :name name :date date :url url}))))
|
||||
(.send web-contents "auto-updater-downloaded" (bean/->js payload))))
|
||||
|
||||
(defn init-auto-updater
|
||||
[repo]
|
||||
(when (.valid semver electron-version)
|
||||
(p/let [info (get-latest-artifact-info repo)]
|
||||
(when-let [remote-version (and info (re-find #"\d+\.\d+\.\d+" (:url info)))]
|
||||
(if (and (. semver valid remote-version)
|
||||
(. semver lt electron-version remote-version))
|
||||
(defn- configure-auto-updater!
|
||||
[]
|
||||
(let [channel (updater-channel)]
|
||||
(when channel
|
||||
(set! (.-channel autoUpdater) channel)
|
||||
;; Keep the original downgrade policy even though setting channel flips it on.
|
||||
(set! (.-allowDowngrade autoUpdater) false))
|
||||
(debug "configure-auto-updater" {:platform (.-platform js/process)
|
||||
:arch (.-arch js/process)
|
||||
:channel channel}))
|
||||
(set! (.-autoInstallOnAppQuit autoUpdater) false)
|
||||
(set! (.-autoDownload autoUpdater) false))
|
||||
|
||||
;; start auto updater
|
||||
(do
|
||||
(debug "Found remote version" remote-version)
|
||||
(when (or mac? win32?)
|
||||
(debug "forward update to autoUpdater")
|
||||
;; FIXME: It seems that update-electron-app doesn't work on linux
|
||||
(when-let [f (js/require "update-electron-app")]
|
||||
(f #js{:notifyUser false})
|
||||
(.once autoUpdater "update-downloaded"
|
||||
new-version-downloaded-cb))))
|
||||
(defn- register-auto-updater-listeners!
|
||||
[^js win]
|
||||
(let [checking-handler
|
||||
(fn []
|
||||
(emit-update! win "checking-for-update" nil))
|
||||
|
||||
(debug "Skip remote version [ahead of pre-release]" remote-version))))))
|
||||
available-handler
|
||||
(fn [info]
|
||||
(emit-update! win "update-available" (normalize-payload info)))
|
||||
|
||||
not-available-handler
|
||||
(fn [info]
|
||||
(emit-update! win "update-not-available" (normalize-payload info))
|
||||
(emit-completed! win))
|
||||
|
||||
progress-handler
|
||||
(fn [progress]
|
||||
(emit-update! win "download-progress" (normalize-payload progress)))
|
||||
|
||||
downloaded-handler
|
||||
(fn [info]
|
||||
(let [payload (normalize-payload info)]
|
||||
(reset! *downloaded-update payload)
|
||||
(logger/info "[update-downloaded]" payload)
|
||||
(emit-update! win "update-downloaded" payload)
|
||||
(emit-update-downloaded! payload)
|
||||
(emit-completed! win)))
|
||||
|
||||
error-handler
|
||||
(fn [error]
|
||||
(logger/warn "[updater/error]" error)
|
||||
(emit-update! win "error" (normalize-error error))
|
||||
(emit-completed! win))]
|
||||
(.on autoUpdater "checking-for-update" checking-handler)
|
||||
(.on autoUpdater "update-available" available-handler)
|
||||
(.on autoUpdater "update-not-available" not-available-handler)
|
||||
(.on autoUpdater "download-progress" progress-handler)
|
||||
(.on autoUpdater "update-downloaded" downloaded-handler)
|
||||
(.on autoUpdater "error" error-handler)
|
||||
#(do
|
||||
(.off autoUpdater "checking-for-update" checking-handler)
|
||||
(.off autoUpdater "update-available" available-handler)
|
||||
(.off autoUpdater "update-not-available" not-available-handler)
|
||||
(.off autoUpdater "download-progress" progress-handler)
|
||||
(.off autoUpdater "update-downloaded" downloaded-handler)
|
||||
(.off autoUpdater "error" error-handler))))
|
||||
|
||||
(defn- <check-for-updates!
|
||||
[^js win auto-download?]
|
||||
(debug "check-for-updates" {:auto-download? auto-download?})
|
||||
(set! (.-autoDownload autoUpdater) auto-download?)
|
||||
(-> (.checkForUpdates autoUpdater)
|
||||
(.then
|
||||
(fn [_]
|
||||
;; Manual checks without auto download need an explicit terminal event.
|
||||
(when-not auto-download?
|
||||
(emit-completed! win))))
|
||||
(.catch
|
||||
(fn [error]
|
||||
(logger/warn "[updater/check]" error)
|
||||
(emit-update! win "error" (normalize-error error))
|
||||
(emit-completed! win)))))
|
||||
|
||||
(defn- init-auto-updater!
|
||||
[^js win]
|
||||
(when (and prod? (not= false (cfgs/get-item :auto-update)))
|
||||
(debug "init-auto-updater")
|
||||
(set! (.-autoDownload autoUpdater) true)
|
||||
(-> (.checkForUpdates autoUpdater)
|
||||
(.catch (fn [error]
|
||||
(logger/warn "[updater/auto-check]" error)
|
||||
(emit-update! win "error" (normalize-error error))
|
||||
(emit-completed! win))))))
|
||||
|
||||
(defn init-updater
|
||||
[{:keys [repo ^js _win] :as opts}]
|
||||
(and prod? (not= false (cfgs/get-item :auto-update)) (init-auto-updater repo))
|
||||
(let [check-channel "check-for-updates"
|
||||
[{:keys [^js win] :as _opts}]
|
||||
(configure-auto-updater!)
|
||||
(let [dispose-listeners! (register-auto-updater-listeners! win)
|
||||
check-channel "check-for-updates"
|
||||
install-channel "install-updates"
|
||||
get-downloaded-channel "get-downloaded-update"
|
||||
check-listener (fn [_e & args]
|
||||
(when-not @*update-pending
|
||||
(reset! *update-pending true)
|
||||
(p/finally
|
||||
(check-for-updates (merge opts {:args args}))
|
||||
#(reset! *update-pending nil))))
|
||||
install-listener (fn [_e quit-app?]
|
||||
(when-let [dest-file (:dest-file @*update-ready-to-install)]
|
||||
(open dest-file)
|
||||
(and quit-app? (js/setTimeout #(.quit app) 1000))))]
|
||||
(let [auto-download? (true? (first args))]
|
||||
(-> (<check-for-updates! win auto-download?)
|
||||
(.finally #(reset! *update-pending nil))))))
|
||||
install-listener (fn [_e _quit-app?]
|
||||
(.quitAndInstall autoUpdater false true))
|
||||
get-downloaded-listener (fn [_e]
|
||||
(some-> @*downloaded-update bean/->js))]
|
||||
(init-auto-updater! win)
|
||||
(.handle ipcMain check-channel check-listener)
|
||||
(.handle ipcMain install-channel install-listener)
|
||||
(.handle ipcMain get-downloaded-channel get-downloaded-listener)
|
||||
#(do
|
||||
(dispose-listeners!)
|
||||
(.removeHandler ipcMain install-channel)
|
||||
(.removeHandler ipcMain check-channel)
|
||||
(.removeHandler ipcMain get-downloaded-channel)
|
||||
(reset! *update-pending nil))))
|
||||
|
||||
@@ -259,10 +259,32 @@
|
||||
[:<>
|
||||
(let [handle-copy!
|
||||
(fn [_e]
|
||||
(-> (util/copy-image-to-clipboard image-src)
|
||||
(p/then #(notification/show! "Copied!" :success))
|
||||
(p/catch (fn [error]
|
||||
(js/console.error error)))))
|
||||
;; Electron renderer cannot fetch file:// URLs; read the
|
||||
;; file via IPC and copy the blob directly.
|
||||
(if (util/electron?)
|
||||
(let [ext (some-> (util/get-file-ext image-src) string/lower-case)
|
||||
;; Should support all exts in common-config/img-formats
|
||||
ext->mime {"png" "image/png"
|
||||
"jpg" "image/jpeg"
|
||||
"jpeg" "image/jpeg"
|
||||
"gif" "image/gif"
|
||||
"webp" "image/webp"
|
||||
"bmp" "image/bmp"
|
||||
"svg" "image/svg+xml"
|
||||
"ico" "image/x-icon"}
|
||||
mime (get ext->mime ext)]
|
||||
(if-not mime
|
||||
(notification/show! (str "Copy image is not supported for ." ext " files") :warning)
|
||||
(-> (p/let [binary (fs/read-file-raw nil image-src {})
|
||||
blob (js/Blob. (array binary) (clj->js {:type mime}))]
|
||||
(util/copy-image-blob-to-clipboard blob))
|
||||
(p/then #(notification/show! "Copied!" :success))
|
||||
(p/catch (fn [error]
|
||||
(js/console.error error))))))
|
||||
(-> (util/copy-image-to-clipboard src')
|
||||
(p/then #(notification/show! "Copied!" :success))
|
||||
(p/catch (fn [error]
|
||||
(js/console.error error))))))
|
||||
handle-delete!
|
||||
(fn [_e]
|
||||
(when-let [block-id (get-blockid)]
|
||||
@@ -569,7 +591,8 @@
|
||||
(if (assets-handler/check-alias-path? href)
|
||||
(assets-handler/normalize-asset-resource-url href)
|
||||
href))]
|
||||
(resizable-image config title href metadata full_text false))))))
|
||||
[:div.as-plain-image-link
|
||||
(resizable-image config title href metadata full_text false)])))))
|
||||
|
||||
(def timestamp-to-string export-common-handler/timestamp-to-string)
|
||||
|
||||
@@ -874,7 +897,9 @@
|
||||
([config format v]
|
||||
(when (string? v)
|
||||
(let [inline-list (gp-mldoc/inline->edn v (mldoc/get-default-config format))]
|
||||
[:div.inline.mr-1 (map-inline config inline-list)]))))
|
||||
[:div.inline
|
||||
(when (get config :add-margin? true) {:class "mr-1"})
|
||||
(map-inline config inline-list)]))))
|
||||
|
||||
(defn- <get-block
|
||||
[block-id]
|
||||
@@ -1190,19 +1215,23 @@
|
||||
|
||||
(defn- render-macro
|
||||
[config name arguments macro-content format]
|
||||
[:div.macro {:data-macro-name name}
|
||||
|
||||
(if macro-content
|
||||
(let [ast (->> (mldoc/->edn macro-content format)
|
||||
(map first))
|
||||
paragraph? (and (= 1 (count ast))
|
||||
(= "Paragraph" (ffirst ast)))]
|
||||
(if (and (not paragraph?)
|
||||
(mldoc/block-with-title? (ffirst ast)))
|
||||
(markup-elements-cp (assoc config :block/format format) ast)
|
||||
(inline-text config format macro-content)))
|
||||
[:span.warning {:title (str "Unsupported macro name: " name)}
|
||||
(macro->text name arguments)])])
|
||||
(into
|
||||
[:div.macro]
|
||||
(let [attributes {:data-macro-name name}]
|
||||
(if macro-content
|
||||
(let [ast (->> (mldoc/->edn macro-content (gp-mldoc/default-config format))
|
||||
(map first))
|
||||
paragraph? (and (= 1 (count ast))
|
||||
(= "Paragraph" (ffirst ast)))]
|
||||
(if (and (not paragraph?)
|
||||
(mldoc/block-with-title? (ffirst ast)))
|
||||
[attributes
|
||||
(markup-elements-cp (assoc config :block/format format) ast)]
|
||||
[(assoc attributes :class "inline")
|
||||
(inline-text {:add-margin? false} format macro-content)]))
|
||||
[attributes
|
||||
[:span.warning {:title (str "Unsupported macro name: " name)}
|
||||
(macro->text name arguments)]]))))
|
||||
|
||||
(rum/defc nested-link < rum/reactive
|
||||
[config html-export? link]
|
||||
@@ -1776,19 +1805,135 @@
|
||||
(apply [(str uuid)])))))
|
||||
|
||||
(declare block-list)
|
||||
(rum/defc block-children < rum/reactive
|
||||
(defn- should-defer-block-children-render?
|
||||
[config children-count anchor]
|
||||
(let [has-anchor? (not (string/blank? anchor))]
|
||||
(and
|
||||
(pos? children-count)
|
||||
(number? (:defer-ready-index config))
|
||||
(:current-page? config)
|
||||
;; Defer only the first level under current page blocks.
|
||||
;; Deeper levels render immediately after their parent subtree is released,
|
||||
;; preserving top-to-bottom perception instead of level-by-level batches.
|
||||
(= 1 (:level config))
|
||||
(not (or has-anchor?
|
||||
(:ref? config)
|
||||
(:custom-query? config)
|
||||
(:sidebar? config)
|
||||
(:embed? config)
|
||||
(:library? config)
|
||||
(:document/mode? config))))))
|
||||
|
||||
;; Progressive root rendering should kick in for pages with either many root
|
||||
;; blocks or heavy expanded descendant trees.
|
||||
(def ^:private defer-root-render-batch-size 1)
|
||||
(def ^:private defer-children-initial-render-budget 12)
|
||||
(def ^:private defer-children-render-batch-budget 12)
|
||||
|
||||
(defn- should-defer-root-block-render?
|
||||
[config root-block blocks anchor]
|
||||
(let [has-anchor? (not (string/blank? anchor))
|
||||
page-blocks-count (count (:block/_page root-block))
|
||||
children-blocks-count (count blocks)]
|
||||
(and
|
||||
(:current-page? config)
|
||||
(zero? (or (:level config) 0))
|
||||
(not (or has-anchor?
|
||||
(:ref? config)
|
||||
(:custom-query? config)
|
||||
(:sidebar? config)
|
||||
(:embed? config)
|
||||
(:library? config)
|
||||
(:document/mode? config)))
|
||||
(>= (- page-blocks-count children-blocks-count) 30))))
|
||||
|
||||
(defn- defer-placeholder-element
|
||||
[]
|
||||
(js/React.createElement "div"
|
||||
#js {:style #js {:minHeight 28}}))
|
||||
|
||||
(defn- deferred-child-render-cost
|
||||
[child]
|
||||
(if (seq (:block/children child)) 4 1))
|
||||
|
||||
(defn- deferred-children-visible-count
|
||||
[children ready-budget]
|
||||
(let [children-count (count children)]
|
||||
(loop [idx 0
|
||||
budget (max 0 ready-budget)]
|
||||
(if (or (>= idx children-count)
|
||||
(<= budget 0))
|
||||
idx
|
||||
(let [cost (deferred-child-render-cost (nth children idx))]
|
||||
(if (>= budget cost)
|
||||
(recur (inc idx) (- budget cost))
|
||||
;; Keep one child visible for progressive feedback.
|
||||
(if (zero? idx) 1 idx)))))))
|
||||
|
||||
(rum/defc block-children
|
||||
[config block children collapsed?]
|
||||
(let [ref? (:ref? config)
|
||||
query? (:custom-query? config)
|
||||
library? (:library? config)
|
||||
children (when (coll? children)
|
||||
(let [ref-matched-children-ids (:ref-matched-children-ids config)]
|
||||
(cond->> (remove nil? children)
|
||||
ref-matched-children-ids
|
||||
;; Block children will not be rendered if the filters do not match them
|
||||
(filter (fn [b] (ref-matched-children-ids (:db/id b))))
|
||||
library?
|
||||
(filter (fn [b] (and (ldb/page? b) (not (or (ldb/class? b) (ldb/property? b)))))))))]
|
||||
(into []
|
||||
(cond->> (remove nil? children)
|
||||
ref-matched-children-ids
|
||||
;; Block children will not be rendered if the filters do not match them
|
||||
(filter (fn [b] (ref-matched-children-ids (:db/id b))))
|
||||
library?
|
||||
(filter (fn [b] (and (ldb/page? b) (not (or (ldb/class? b) (ldb/property? b))))))))))
|
||||
children-count (count children)
|
||||
anchor (get-in (state/get-route-match) [:query-params :anchor])
|
||||
defer-render? (should-defer-block-children-render? config children-count anchor)
|
||||
defer-ready-index (:defer-ready-index config)
|
||||
defer-index (or (:defer-top-index config) 0)
|
||||
render-children? (or (not defer-render?)
|
||||
(<= defer-index defer-ready-index))
|
||||
*defer-children-render-complete-by-root (:defer-children-render-complete-by-root* config)
|
||||
fallback-children-ready-budget* (hooks/use-memo #(atom defer-children-initial-render-budget) [])
|
||||
*children-ready-budget fallback-children-ready-budget*
|
||||
[children-ready-budget] (hooks/use-atom *children-ready-budget)
|
||||
visible-children-count (if defer-render?
|
||||
(deferred-children-visible-count children children-ready-budget)
|
||||
children-count)
|
||||
children-max-render-budget (* children-count 4)
|
||||
children-fully-rendered? (or collapsed?
|
||||
(>= visible-children-count children-count))
|
||||
children' (if (and defer-render? render-children?)
|
||||
(subvec children 0 visible-children-count)
|
||||
children)]
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when (and defer-render?
|
||||
render-children?
|
||||
(number? defer-index)
|
||||
*defer-children-render-complete-by-root)
|
||||
(swap! *defer-children-render-complete-by-root
|
||||
assoc
|
||||
defer-index
|
||||
children-fully-rendered?))
|
||||
(if (and defer-render?
|
||||
render-children?
|
||||
(< visible-children-count children-count))
|
||||
(let [raf-id (js/requestAnimationFrame
|
||||
(fn []
|
||||
(swap! *children-ready-budget
|
||||
(fn [v]
|
||||
(min children-max-render-budget
|
||||
(+ v defer-children-render-batch-budget))))))]
|
||||
#(js/cancelAnimationFrame raf-id))
|
||||
(fn [])))
|
||||
[defer-render?
|
||||
render-children?
|
||||
defer-index
|
||||
children-fully-rendered?
|
||||
visible-children-count
|
||||
children-count
|
||||
children-max-render-budget
|
||||
*children-ready-budget
|
||||
*defer-children-render-complete-by-root])
|
||||
(when (and (coll? children)
|
||||
(seq children)
|
||||
(not collapsed?))
|
||||
@@ -1804,7 +1949,8 @@
|
||||
(assoc :block-children? true)
|
||||
(integer? (:block-level config))
|
||||
(update :block-level inc))]
|
||||
(block-list config' children))]])))
|
||||
(when render-children?
|
||||
(block-list config' children')))]])))
|
||||
|
||||
(defn- block-content-empty?
|
||||
[block]
|
||||
@@ -1991,8 +2137,6 @@
|
||||
item-content (.. target -nextSibling -data)]
|
||||
(editor-handler/toggle-list-checkbox block item-content)))}))
|
||||
|
||||
(declare block-content)
|
||||
|
||||
(declare src-cp)
|
||||
|
||||
(rum/defc ^:large-vars/cleanup-todo text-block-title
|
||||
@@ -3643,8 +3787,6 @@
|
||||
(gp-mldoc/inline->edn title
|
||||
(mldoc/get-default-config :markdown))))
|
||||
|
||||
(declare ->hiccup)
|
||||
|
||||
(defn- get-code-mode-by-lang
|
||||
[lang]
|
||||
(some (fn [m] (when (= (.-name m) lang) (.-mode m))) js/window.CodeMirror.modeInfo))
|
||||
@@ -3794,8 +3936,9 @@
|
||||
[:div.warning "#+BEGIN_QUOTE is deprecated. Use '/Quote' command instead."]
|
||||
["Raw_Html" content]
|
||||
(when (not html-export?)
|
||||
[:div.raw_html {:dangerouslySetInnerHTML
|
||||
{:__html (security/sanitize-html content)}}])
|
||||
[:div.raw_html.inline-block
|
||||
{:dangerouslySetInnerHTML
|
||||
{:__html (security/sanitize-html content)}}])
|
||||
["Export" "html" _options content]
|
||||
(when (not html-export?)
|
||||
[:div.export_html {:dangerouslySetInnerHTML
|
||||
@@ -3804,8 +3947,9 @@
|
||||
(ui/catch-error
|
||||
[:div.warning {:title "Invalid hiccup"}
|
||||
content]
|
||||
[:div.hiccup_html {:dangerouslySetInnerHTML
|
||||
{:__html (hiccup->html content)}}])
|
||||
[:div.hiccup_html.inline
|
||||
{:dangerouslySetInnerHTML
|
||||
{:__html (hiccup->html content)}}])
|
||||
|
||||
["Export" "latex" _options content]
|
||||
(if html-export?
|
||||
@@ -3917,24 +4061,59 @@
|
||||
(when linked-block
|
||||
(str "-" (:block/uuid original-block))))))))
|
||||
|
||||
(rum/defc block-list
|
||||
(rum/defc ^:large-vars/cleanup-todo block-list
|
||||
[config blocks]
|
||||
(let [[virtualized? _] (hooks/use-state (not (or (util/rtc-test?)
|
||||
(let [blocks-count (count blocks)
|
||||
root-block (when-let [id (:db/id config)]
|
||||
(db/entity id))
|
||||
[virtualized? _] (hooks/use-state (not (or (util/rtc-test?)
|
||||
(and (util/mobile?) (:journals? config))
|
||||
(if (:journals? config)
|
||||
(< (count blocks) 50)
|
||||
(< (count blocks) 10))
|
||||
(< blocks-count 50)
|
||||
(< blocks-count 10))
|
||||
(and (:block-children? config)
|
||||
;; zoom-in block's children
|
||||
(not (and (:id config) (= (:id config) (str (:block/uuid (:block/parent (first blocks)))))))))))
|
||||
root-level? (zero? (or (:level config) 0))
|
||||
anchor (get-in (state/get-route-match) [:query-params :anchor])
|
||||
fallback-ready-index* (hooks/use-memo #(atom -1) [])
|
||||
fallback-children-complete-by-root* (hooks/use-memo #(atom {}) [])
|
||||
*defer-ready-index (or (:defer-children-ready-index* config)
|
||||
fallback-ready-index*)
|
||||
*defer-children-render-complete-by-root (or (:defer-children-render-complete-by-root* config)
|
||||
fallback-children-complete-by-root*)
|
||||
[defer-ready-index] (hooks/use-atom *defer-ready-index)
|
||||
[defer-children-render-complete-by-root] (hooks/use-atom *defer-children-render-complete-by-root)
|
||||
defer-root-render? (should-defer-root-block-render? config root-block blocks anchor)
|
||||
current-root-block (when (<= 0 defer-ready-index (dec blocks-count))
|
||||
(nth blocks defer-ready-index))
|
||||
current-root-children-need-deferring? (and current-root-block
|
||||
(not (util/collapsed? current-root-block))
|
||||
(should-defer-block-children-render?
|
||||
(assoc config :level 1 :defer-ready-index defer-ready-index)
|
||||
(count (:block/children current-root-block))
|
||||
anchor))
|
||||
current-root-children-rendered? (or (neg? defer-ready-index)
|
||||
(not current-root-children-need-deferring?)
|
||||
(true? (get defer-children-render-complete-by-root
|
||||
defer-ready-index)))
|
||||
root-item-visible? (fn [idx]
|
||||
(or (not defer-root-render?)
|
||||
(<= idx defer-ready-index)))
|
||||
render-item (fn [idx]
|
||||
(let [top? (zero? idx)
|
||||
bottom? (= (dec (count blocks)) idx)
|
||||
block (nth blocks idx)]
|
||||
(block-item (assoc config :top? top?)
|
||||
block
|
||||
{:top? top?
|
||||
:bottom? bottom?})))
|
||||
bottom? (= (dec blocks-count) idx)
|
||||
block (nth blocks idx)
|
||||
config' (cond-> (assoc config :top? top?)
|
||||
(and root-level? defer-root-render?) (assoc :defer-top-index idx)
|
||||
(and root-level? defer-root-render?) (assoc :defer-ready-index defer-ready-index)
|
||||
(and root-level? defer-root-render?) (assoc :defer-children-render-complete-by-root* *defer-children-render-complete-by-root))]
|
||||
(if (and root-level? (not (root-item-visible? idx)))
|
||||
(defer-placeholder-element)
|
||||
(block-item config'
|
||||
block
|
||||
{:top? top?
|
||||
:bottom? bottom?}))))
|
||||
virtualized? (and virtualized? (seq blocks))
|
||||
*virtualized-ref (hooks/use-ref nil)
|
||||
virtual-opts (when virtualized?
|
||||
@@ -3949,16 +4128,47 @@
|
||||
;; Leave some space for the new inserted block
|
||||
:increase-viewport-by 254
|
||||
:overscan 254
|
||||
:total-count (count blocks)
|
||||
:total-count blocks-count
|
||||
:item-content (fn [idx]
|
||||
(let [top? (zero? idx)
|
||||
bottom? (= (dec (count blocks)) idx)
|
||||
block (nth blocks idx)]
|
||||
(block-item (assoc config :top? top?)
|
||||
block
|
||||
{:top? top?
|
||||
:bottom? bottom?})))})
|
||||
bottom? (= (dec blocks-count) idx)
|
||||
block (nth blocks idx)
|
||||
config' (cond-> (assoc config :top? top?)
|
||||
(and root-level? defer-root-render?) (assoc :defer-top-index idx)
|
||||
(and root-level? defer-root-render?) (assoc :defer-ready-index defer-ready-index)
|
||||
(and root-level? defer-root-render?) (assoc :defer-children-render-complete-by-root* *defer-children-render-complete-by-root))]
|
||||
(if (and root-level? (not (root-item-visible? idx)))
|
||||
(defer-placeholder-element)
|
||||
(block-item config'
|
||||
block
|
||||
{:top? top?
|
||||
:bottom? bottom?}))))})
|
||||
*wrap-ref (hooks/use-ref nil)]
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(let [last-idx (dec blocks-count)]
|
||||
(if (and defer-root-render?
|
||||
current-root-children-rendered?
|
||||
(< defer-ready-index last-idx))
|
||||
(let [raf-id (js/requestAnimationFrame
|
||||
(fn []
|
||||
(swap! *defer-ready-index
|
||||
(fn [v]
|
||||
(let [next-v (min (dec blocks-count)
|
||||
(+ v defer-root-render-batch-size))]
|
||||
(swap! *defer-children-render-complete-by-root
|
||||
assoc
|
||||
next-v
|
||||
false)
|
||||
next-v)))))]
|
||||
#(js/cancelAnimationFrame raf-id))
|
||||
(fn []))))
|
||||
[defer-root-render?
|
||||
defer-ready-index
|
||||
current-root-children-rendered?
|
||||
blocks-count
|
||||
*defer-ready-index
|
||||
*defer-children-render-complete-by-root])
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when virtualized?
|
||||
@@ -3999,10 +4209,15 @@
|
||||
|
||||
(rum/defcs blocks-container < mixins/container-id rum/static
|
||||
{:init (fn [state]
|
||||
(assoc state ::id (str (random-uuid))))}
|
||||
(assoc state
|
||||
::id (str (random-uuid))
|
||||
::defer-children-ready-index* (atom -1)
|
||||
::defer-children-render-complete-by-root* (atom {})))}
|
||||
[state config blocks]
|
||||
(let [doc-mode? (:document/mode? config)
|
||||
id (::id state)]
|
||||
id (::id state)
|
||||
*defer-children-ready-index (::defer-children-ready-index* state)
|
||||
*defer-children-render-complete-by-root (::defer-children-render-complete-by-root* state)]
|
||||
(when (seq blocks)
|
||||
[:div.blocks-container.flex-1
|
||||
{:id id
|
||||
@@ -4010,6 +4225,8 @@
|
||||
:containerid (:container-id state)}
|
||||
(block-list (assoc config
|
||||
:blocks-node-id id
|
||||
:defer-children-ready-index* *defer-children-ready-index
|
||||
:defer-children-render-complete-by-root* *defer-children-render-complete-by-root
|
||||
:container-id (:container-id state))
|
||||
blocks)])))
|
||||
|
||||
|
||||
@@ -158,6 +158,9 @@
|
||||
.block-children-container {
|
||||
position: relative;
|
||||
margin-left: 29px;
|
||||
/* Keep vertical rhythm stable when a sibling becomes a nested child. */
|
||||
padding-top: 0.125rem;
|
||||
margin-bottom: -0.125rem;
|
||||
}
|
||||
|
||||
.block-children-left-border {
|
||||
@@ -1224,8 +1227,20 @@ html.is-mac {
|
||||
}
|
||||
}
|
||||
|
||||
.property-value .ls-resize-image {
|
||||
max-width: 35%;
|
||||
.property-value {
|
||||
.ls-resize-image {
|
||||
max-width: 35%;
|
||||
}
|
||||
|
||||
.as-plain-image-link {
|
||||
.ls-resize-image {
|
||||
max-width: 80px;
|
||||
}
|
||||
|
||||
.image-resize {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.recent-block {
|
||||
|
||||
@@ -9,6 +9,33 @@
|
||||
@apply p-0 w-auto !max-w-fit overflow-hidden;
|
||||
}
|
||||
|
||||
/* Remove input focus glow/ring for cmdk search box only */
|
||||
.cp__cmdk-input-row:focus-within,
|
||||
.cp__cmdk-search-input,
|
||||
.cp__cmdk-search-input:focus,
|
||||
.cp__cmdk-search-input:focus-visible {
|
||||
box-shadow: none !important;
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
.cp__cmdk-search-input:focus,
|
||||
.cp__cmdk-search-input:focus-visible {
|
||||
border-color: transparent !important;
|
||||
--tw-ring-offset-shadow: 0 0 #0000;
|
||||
--tw-ring-shadow: 0 0 #0000;
|
||||
}
|
||||
|
||||
/* Cmd+K input text selection color should stay theme-neutral in dark mode */
|
||||
.cp__cmdk-search-input::selection {
|
||||
color: var(--lx-gray-12, var(--ls-primary-text-color, inherit));
|
||||
background: var(--lx-gray-06, var(--ls-tertiary-background-color, rgba(255, 255, 255, 0.16)));
|
||||
}
|
||||
|
||||
.cp__cmdk-search-input::-moz-selection {
|
||||
color: var(--lx-gray-12, var(--ls-primary-text-color, inherit));
|
||||
background: var(--lx-gray-06, var(--ls-tertiary-background-color, rgba(255, 255, 255, 0.16)));
|
||||
}
|
||||
|
||||
.cp__cmdk-current-page-badge {
|
||||
@apply inline-flex items-center rounded-full border text-xs font-medium leading-none;
|
||||
padding: 2px 8px;
|
||||
@@ -17,20 +44,32 @@
|
||||
border-color: var(--lx-gray-06, var(--ls-border-color, rgba(0, 0, 0, 0.12)));
|
||||
}
|
||||
|
||||
[data-cmdk-item] {
|
||||
border-radius: 0.5rem;
|
||||
margin-inline: 2px;
|
||||
}
|
||||
|
||||
/* Keyboard navigation highlight */
|
||||
[data-cmdk-item][data-kb-highlighted] {
|
||||
background-color: var(--lx-gray-03, var(--ls-a-chosen-bg, var(--ls-tertiary-background-color, rgba(0, 0, 0, 0.10))));
|
||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.07), inset 0 0 0 2px var(--lx-accent-09, #3b82f6);
|
||||
box-shadow: inset 0 0 0 9999px rgba(0, 0, 0, 0.07), inset 0 0 0 1px var(--lx-accent-03, #3b82f6);
|
||||
}
|
||||
|
||||
/* Mouse hover — item not selected */
|
||||
[data-cmdk-item][data-hoverable]:not([data-highlighted]):hover {
|
||||
background-color: var(--lx-gray-03, var(--ls-a-chosen-bg, var(--ls-tertiary-background-color, rgba(0, 0, 0, 0.10))));
|
||||
box-shadow: inset 0 0 0 1px var(--ls-border-color, var(--lx-gray-08, rgba(0, 0, 0, 0.24)));
|
||||
box-shadow: inset 0 0 0 1px var(--ls-border-color, var(--lx-gray-03, rgba(0, 0, 0, 0.24)));
|
||||
}
|
||||
|
||||
/* Mouse hover — item selected (stronger border) */
|
||||
/* Mouse hover — item selected */
|
||||
[data-cmdk-item][data-hoverable][data-highlighted]:hover {
|
||||
background-color: var(--lx-gray-03, var(--ls-a-chosen-bg, var(--ls-tertiary-background-color, rgba(0, 0, 0, 0.10))));
|
||||
box-shadow: inset 0 0 0 1px var(--ls-border-color, var(--lx-gray-09, rgba(0, 0, 0, 0.32)));
|
||||
}
|
||||
|
||||
/* Dark theme: remove cmdk item outline/border effect */
|
||||
.dark [data-cmdk-item][data-kb-highlighted],
|
||||
.dark [data-cmdk-item][data-hoverable]:not([data-highlighted]):hover,
|
||||
.dark [data-cmdk-item][data-hoverable][data-highlighted]:hover {
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -1075,9 +1075,9 @@
|
||||
(when timeout-id
|
||||
(js/clearTimeout timeout-id)))))
|
||||
[])
|
||||
[:div {:class "bg-gray-02 border-b border-1 border-gray-07"}
|
||||
[:div.cp__cmdk-input-row {:class "bg-gray-02 border-b border-1 border-gray-07"}
|
||||
[:input.cp__cmdk-search-input
|
||||
{:class "text-xl bg-transparent border-none w-full outline-none px-3 py-3"
|
||||
{:class "text-xl bg-transparent !border-none w-full !outline-none !shadow-none px-3 py-3 focus:!border-none focus:!outline-none focus:!shadow-none focus-visible:!outline-none focus-visible:!shadow-none focus:ring-0 focus:ring-offset-0 focus-visible:ring-0 focus-visible:ring-offset-0"
|
||||
:auto-focus true
|
||||
:autoComplete "off"
|
||||
:autoCapitalize "off"
|
||||
@@ -1299,4 +1299,3 @@
|
||||
(rum/defc cmdk-block [props]
|
||||
[:div {:class "cp__cmdk__block rounded-md"}
|
||||
(cmdk props)])
|
||||
|
||||
|
||||
@@ -220,7 +220,6 @@
|
||||
|
||||
(rum/defc help-menu-popup
|
||||
[]
|
||||
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(state/set-state! :ui/handbooks-open? false))
|
||||
@@ -273,6 +272,16 @@
|
||||
(when handbooks-open?
|
||||
(handbooks/handbooks-popup))]))
|
||||
|
||||
(defn- context-menu-click-should-hide?
|
||||
[target]
|
||||
(let [menu-item (some-> target (.closest "[role='menuitem']"))
|
||||
submenu-trigger? (= "menu" (some-> menu-item (.getAttribute "aria-haspopup")))]
|
||||
(boolean
|
||||
(and target
|
||||
(not (util/input? target))
|
||||
menu-item
|
||||
(not submenu-trigger?)))))
|
||||
|
||||
(rum/defc app-context-menu-observer
|
||||
< rum/static
|
||||
(mixins/event-mixin
|
||||
@@ -292,10 +301,8 @@
|
||||
(fn [{:keys [id]}]
|
||||
[:div {:on-click (fn [^js e]
|
||||
(when-let [target (.-target e)]
|
||||
(let [input? (util/input? target)
|
||||
popup? (.closest target "[data-radix-popper-content-wrapper]")]
|
||||
(when (not (or input? popup?))
|
||||
(shui/popup-hide! id)))))
|
||||
(when (context-menu-click-should-hide? target)
|
||||
(shui/popup-hide! id))))
|
||||
:data-keep-selection true}
|
||||
content])
|
||||
(merge
|
||||
|
||||
@@ -2,11 +2,13 @@
|
||||
(:require [cljs-time.coerce :as tc]
|
||||
[cljs.pprint :as pp]
|
||||
[clojure.string :as string]
|
||||
[electron.ipc :as ipc]
|
||||
[frontend.commands :as commands]
|
||||
[frontend.components.editor :as editor]
|
||||
[frontend.components.export :as export]
|
||||
[frontend.components.icon :as icon-component]
|
||||
[frontend.components.page-menu :as page-menu]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.db :as db]
|
||||
[frontend.extensions.fsrs :as fsrs]
|
||||
@@ -23,6 +25,7 @@
|
||||
[frontend.util.url :as url-util]
|
||||
[goog.dom :as gdom]
|
||||
[goog.object :as gobj]
|
||||
[logseq.common.path :as path]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.shui.ui :as shui]
|
||||
@@ -240,6 +243,16 @@
|
||||
(editor-handler/copy-block-ref! block-id tap-f)))}
|
||||
(t :content/copy-block-url)))
|
||||
|
||||
(when (and (util/electron?) (ldb/asset? block))
|
||||
(shui/dropdown-menu-item
|
||||
{:key "Show asset in folder"
|
||||
:on-click (fn [_e]
|
||||
(let [assets-dir (config/get-current-repo-assets-root)
|
||||
ext (name (:logseq.property.asset/type block))
|
||||
file-path (path/path-join assets-dir (str (:block/uuid block) "." ext))]
|
||||
(ipc/ipc "openFileInFolder" file-path)))}
|
||||
(t :asset/show-file-in-folder)))
|
||||
|
||||
(shui/dropdown-menu-item
|
||||
{:key "Copy as"
|
||||
:on-click (fn [_]
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
[cljs-time.core :as t]
|
||||
[clojure.string :as string]
|
||||
[dommy.core :as d]
|
||||
[electron.ipc :as ipc]
|
||||
[frontend.common.missionary :as c.m]
|
||||
[frontend.components.block :as component-block]
|
||||
[frontend.components.export :as export]
|
||||
@@ -35,6 +36,7 @@
|
||||
[logseq.shui.ui :as shui]
|
||||
[logseq.shui.util :as shui-util]
|
||||
[missionary.core :as m]
|
||||
[promesa.core :as p]
|
||||
[reitit.frontend.easy :as rfe]
|
||||
[rum.core :as rum]))
|
||||
|
||||
@@ -244,8 +246,17 @@
|
||||
(let [[downloaded, set-downloaded] (rum/use-state nil)
|
||||
_ (hooks/use-effect!
|
||||
(fn []
|
||||
(when-let [channel (and (util/electron?) "auto-updater-downloaded")]
|
||||
(let [callback (fn [_ args]
|
||||
(when (util/electron?)
|
||||
(-> (ipc/invoke "get-downloaded-update")
|
||||
(p/then
|
||||
(fn [args]
|
||||
(when args
|
||||
(let [args (bean/->clj args)]
|
||||
(set-downloaded args)
|
||||
(state/set-state! :electron/auto-updater-downloaded args)))))
|
||||
(p/catch (fn [_] nil)))
|
||||
(let [channel "auto-updater-downloaded"
|
||||
callback (fn [_ args]
|
||||
(js/console.debug "[new-version downloaded] args:" args)
|
||||
(let [args (bean/->clj args)]
|
||||
(set-downloaded args)
|
||||
@@ -347,11 +358,25 @@
|
||||
(ldb/page? page) (:block/parent page))
|
||||
[:div.ls-block-breadcrumb
|
||||
[:div.text-sm
|
||||
(component-block/breadcrumb {}
|
||||
(component-block/breadcrumb {}
|
||||
(state/get-current-repo)
|
||||
(:block/uuid page)
|
||||
{:header? true})]])))
|
||||
|
||||
(rum/defc search-index-progress < rum/reactive
|
||||
[]
|
||||
(let [current-repo (state/get-current-repo)
|
||||
{:keys [running? repo progress]} (or (state/sub :search/index-build) {})
|
||||
progress' (-> (or progress 0)
|
||||
(max 0)
|
||||
(min 100))]
|
||||
(when (and running? (= repo current-repo))
|
||||
[:div.search-index-progress
|
||||
[ui/loading ""]
|
||||
[:span.search-index-progress__text (str "Indexing " progress' "%")]
|
||||
[:div.search-index-progress__bar
|
||||
[:div.search-index-progress__bar-fill {:style {:width (str progress' "%")}}]]])))
|
||||
|
||||
(rum/defc ^:large-vars/cleanup-todo header-aux < rum/reactive
|
||||
[{:keys [current-repo default-home new-block-mode]}]
|
||||
(let [electron-mac? (and util/mac? (util/electron?))
|
||||
@@ -414,6 +439,7 @@
|
||||
(rtc-indicator/downloading-detail))
|
||||
(when (user-handler/logged-in?)
|
||||
(rtc-indicator/uploading-detail))
|
||||
(search-index-progress)
|
||||
|
||||
(when (and (not= (state/get-current-route) :home)
|
||||
(not custom-home-page?))
|
||||
|
||||
@@ -341,4 +341,24 @@ html.is-zoomed-native-ios {
|
||||
max-width: 34ch;
|
||||
}
|
||||
}
|
||||
|
||||
.search-index-progress {
|
||||
@apply flex items-center gap-2 rounded px-2 py-1 text-xs opacity-90;
|
||||
-webkit-app-region: no-drag;
|
||||
background-color: var(--ls-tertiary-background-color);
|
||||
}
|
||||
|
||||
.search-index-progress__text {
|
||||
@apply whitespace-nowrap;
|
||||
}
|
||||
|
||||
.search-index-progress__bar {
|
||||
@apply h-1 w-16 overflow-hidden rounded;
|
||||
background-color: var(--ls-quaternary-background-color);
|
||||
}
|
||||
|
||||
.search-index-progress__bar-fill {
|
||||
@apply h-full transition-all duration-200;
|
||||
background-color: var(--ls-link-text-color);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,6 +157,16 @@
|
||||
(add-button-inner block (assoc config :editing? editing?))))
|
||||
|
||||
(rum/defcs page-blocks-cp < rum/reactive db-mixins/query
|
||||
{:did-mount (fn [state]
|
||||
(when-let [on-page-blocks-rendered (some-> (last (:rum/args state))
|
||||
:on-page-blocks-rendered)]
|
||||
(on-page-blocks-rendered))
|
||||
state)
|
||||
:did-update (fn [state]
|
||||
(when-let [on-page-blocks-rendered (some-> (last (:rum/args state))
|
||||
:on-page-blocks-rendered)]
|
||||
(on-page-blocks-rendered))
|
||||
state)}
|
||||
[state block* {:keys [sidebar? hide-add-button? journals?] :as config}]
|
||||
(when-let [id (:db/id block*)]
|
||||
(let [block (db/sub-block id)
|
||||
@@ -337,6 +347,18 @@
|
||||
(when-let [path-page-name (get-path-page-name state page-name)]
|
||||
(util/page-name-sanity-lc path-page-name)))
|
||||
|
||||
(rum/defcs on-mounted <
|
||||
{:did-mount (fn [state]
|
||||
(when-let [f (last (:rum/args state))]
|
||||
(f))
|
||||
state)
|
||||
:did-update (fn [state]
|
||||
(when-let [f (last (:rum/args state))]
|
||||
(f))
|
||||
state)}
|
||||
[state child _on-mounted]
|
||||
child)
|
||||
|
||||
(rum/defc lsp-pagebar-slot <
|
||||
rum/static
|
||||
[]
|
||||
@@ -382,11 +404,13 @@
|
||||
(when class?
|
||||
(shui/tabs-content
|
||||
{:value "tag"}
|
||||
(objects/class-objects page opts)))
|
||||
(on-mounted (objects/class-objects page opts)
|
||||
(:on-tagged-nodes-rendered opts))))
|
||||
(when property?
|
||||
(shui/tabs-content
|
||||
{:value "property"}
|
||||
(objects/property-related-objects page opts))))]))
|
||||
(on-mounted (objects/property-related-objects page opts)
|
||||
(:on-tagged-nodes-rendered opts)))))]))
|
||||
|
||||
(rum/defc sidebar-page-properties
|
||||
[config page]
|
||||
@@ -408,9 +432,14 @@
|
||||
;; A page is just a logical block
|
||||
(rum/defcs ^:large-vars/cleanup-todo page-inner < rum/reactive db-mixins/query mixins/container-id
|
||||
(rum/local nil ::current-page)
|
||||
(rum/local nil ::linked-refs-blocks-ready-page-id)
|
||||
(rum/local nil ::linked-refs-tagged-ready-page-id)
|
||||
[state {:keys [repo page preview? sidebar? tag-dialog? linked-refs? unlinked-refs? config journals?] :as option}]
|
||||
(let [current-repo (state/sub :git/current-repo)
|
||||
linked-refs-blocks-ready-page-id (get state ::linked-refs-blocks-ready-page-id)
|
||||
linked-refs-tagged-ready-page-id (get state ::linked-refs-tagged-ready-page-id)
|
||||
page (or page (some-> (:db/id option) db/entity))
|
||||
page-id (:db/id page)
|
||||
config (assoc config
|
||||
:id (str (:block/uuid page)))
|
||||
repo (or repo current-repo)
|
||||
@@ -427,7 +456,14 @@
|
||||
(= title (date/journal-name)))
|
||||
home? (= :home (state/get-current-route))
|
||||
recycled? (ldb/recycled? page)
|
||||
show-tabs? (and (or class-page? (ldb/property? page)) (not tag-dialog?))]
|
||||
show-tabs? (and (or class-page? (ldb/property? page)) (not tag-dialog?))
|
||||
blocks-ready? (or journals?
|
||||
(= page-id @linked-refs-blocks-ready-page-id))
|
||||
tagged-ready? (or (not show-tabs?)
|
||||
(= page-id @linked-refs-tagged-ready-page-id)
|
||||
;; Fallback to avoid blocking refs forever when tab content is reused.
|
||||
(= page-id @linked-refs-blocks-ready-page-id))
|
||||
linked-refs-ready? (and blocks-ready? tagged-ready?)]
|
||||
(if page
|
||||
(when (or title block?)
|
||||
(if recycled?
|
||||
@@ -441,9 +477,10 @@
|
||||
{:data-page-tags (text-util/build-data-value page-names)}))
|
||||
{})
|
||||
|
||||
{:key title
|
||||
:class (util/classnames [{:is-journals (or journal? fmt-journal?)
|
||||
:is-node-page (or class-page? property-page?)}])})
|
||||
{:key title
|
||||
:class (util/classnames [{:is-journals (or journal? fmt-journal?)
|
||||
:is-today-page (and (not home?) (boolean today?))
|
||||
:is-node-page (or class-page? property-page?)}])})
|
||||
|
||||
[:div.relative.grid.gap-4.sm:gap-8.page-inner.mb-16
|
||||
(when-not (or block? sidebar?)
|
||||
@@ -467,16 +504,21 @@
|
||||
(sidebar-page-properties config page)])
|
||||
|
||||
(when show-tabs?
|
||||
(tabs page {:current-page? option :sidebar? sidebar?}))
|
||||
(tabs page {:current-page? option
|
||||
:sidebar? sidebar?
|
||||
:on-tagged-nodes-rendered #(when-not (= @linked-refs-tagged-ready-page-id page-id)
|
||||
(reset! linked-refs-tagged-ready-page-id page-id))}))
|
||||
|
||||
(when (not tag-dialog?)
|
||||
(if recycle-page?
|
||||
(recycle/recycle-page page)
|
||||
(recycle/recycle-page page {:class "ls-recycle-page-title-compact"})
|
||||
[:div.ls-page-blocks
|
||||
{:style {:margin-left (if (util/mobile?) 0 -20)}
|
||||
:class (when-not (or sidebar? (util/capacitor?))
|
||||
"mt-4")}
|
||||
(page-blocks-cp page (merge option {:sidebar? sidebar?
|
||||
:on-page-blocks-rendered #(when-not (= @linked-refs-blocks-ready-page-id page-id)
|
||||
(reset! linked-refs-blocks-ready-page-id page-id))
|
||||
:container-id (:container-id state)}))]))]
|
||||
|
||||
(when-not (or preview? recycle-page?)
|
||||
@@ -492,7 +534,9 @@
|
||||
(class-component/class-children page))
|
||||
|
||||
;; referenced blocks
|
||||
(when-not (or tag-dialog? linked-refs?)
|
||||
(when (and linked-refs-ready?
|
||||
(not tag-dialog?)
|
||||
(not linked-refs?))
|
||||
[:div.fade-in.delay {:key "page-references"}
|
||||
(rum/with-key
|
||||
(reference/references page {:sidebar? sidebar?
|
||||
@@ -994,7 +1038,7 @@
|
||||
(ui/icon "alert-triangle")]]
|
||||
[:div.mt-3.text-center.sm:mt-0.sm:ml-4.sm:text-left
|
||||
[:h3#modal-headline.text-lg.leading-6.font-medium
|
||||
(t :page/delete-confirmation)]]]
|
||||
(t :page/batch-delete-confirmation)]]]
|
||||
|
||||
[:ol.p-2.pt-4
|
||||
(for [page-item pages]
|
||||
|
||||
@@ -240,6 +240,16 @@ html.is-native-ios {
|
||||
@apply min-h-[60px] overflow-hidden;
|
||||
}
|
||||
|
||||
/* Recycle page only: reduce the gap between "Recycle" title and its description label.
|
||||
Scoped class so other pages are unaffected. */
|
||||
.ls-recycle-page-title-compact {
|
||||
margin-top: var(--ls-recycle-page-title-description-gap-offset, -1.25rem);
|
||||
}
|
||||
|
||||
.ls-recycle-page-description {
|
||||
margin-bottom: var(--ls-recycle-page-description-margin-bottom, 1rem);
|
||||
}
|
||||
|
||||
.block-add-button {
|
||||
&.selected {
|
||||
opacity: 1 !important;
|
||||
|
||||
@@ -71,7 +71,9 @@
|
||||
{:title [:h3.text-lg.leading-6.font-medium.flex.gap-2.items-center
|
||||
[:span.top-1.relative
|
||||
(shui/tabler-icon "alert-triangle")]
|
||||
(t :page/db-delete-confirmation)]
|
||||
(if (or (ldb/class? page) (ldb/property? page))
|
||||
(t :page/permanently-delete-confirmation)
|
||||
(t :page/db-delete-confirmation))]
|
||||
:content [:p.opacity-60 (str "- " (:block/title page))]
|
||||
:outside-cancel? true})
|
||||
(p/then #(delete-page! page))
|
||||
|
||||
@@ -141,7 +141,7 @@
|
||||
(fn [^js e]
|
||||
(case (keyword (aget e "name"))
|
||||
:IllegalPluginPackageError
|
||||
(notification/show! "Illegal Logseq plugin package." :error)
|
||||
(plugin-handler/show-illegal-plugin-package-notification! e)
|
||||
:ExistedImportedPluginPackageError
|
||||
(notification/show! (str "Existed plugin package (" (.-message e) ").") :error)
|
||||
:default)
|
||||
@@ -1580,6 +1580,35 @@
|
||||
:align :start
|
||||
:id "ls-focused-settings-modal"}))
|
||||
|
||||
;; tools for user registered host renderers
|
||||
(rum/defc renderer-container
|
||||
[{:keys [_pid render] :as opts}]
|
||||
(if (fn? render)
|
||||
[:div.lsp-host-renderer-container
|
||||
(render opts)]
|
||||
[:pre (pr-str opts)]))
|
||||
|
||||
(rum/defc renderer-resolver < rum/static
|
||||
[nskey']
|
||||
(when-let [pid (some-> nskey' (namespace))]
|
||||
(let [key (name nskey')
|
||||
[renderer set-renderer!] (rum/use-state nil)]
|
||||
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(try
|
||||
(when-let [renderer (plugin-handler/resolve-hosted-render pid key :sidebar)]
|
||||
(let [r (bean/->clj renderer)
|
||||
title (:title r)]
|
||||
(when-let [^js dom (and title (js/document.getElementById nskey'))]
|
||||
(set! (. dom -textContent) title))
|
||||
(set-renderer! r)))
|
||||
(catch js/Error e (js/console.error "Failed to resolve renderer:" nskey' e))))
|
||||
[pid key])
|
||||
|
||||
(when renderer
|
||||
(renderer-container renderer)))))
|
||||
|
||||
(defn hook-custom-routes
|
||||
[routes]
|
||||
(cond-> routes
|
||||
|
||||
@@ -975,3 +975,7 @@ body[data-page=plugins] {
|
||||
padding-right: 30px;
|
||||
}
|
||||
}
|
||||
|
||||
.lsp-host-renderer-container {
|
||||
user-select: text;
|
||||
}
|
||||
@@ -546,7 +546,8 @@
|
||||
[:div.property-key property-key-cp'])
|
||||
|
||||
(let [property-desc (when-not (= (:db/ident property) :logseq.property/description)
|
||||
(:logseq.property/description property))]
|
||||
(:logseq.property/description property))
|
||||
block' (assoc block (:db/ident property) v)]
|
||||
[:div.ls-block.property-value-container.flex.flex-row.gap-1
|
||||
{:class (if (contains? #{:checkbox :date :datetime} type)
|
||||
"items-center"
|
||||
@@ -560,7 +561,67 @@
|
||||
[:div.property-value.flex.flex-1
|
||||
(if (:class-schema? opts)
|
||||
(pv/property-value property (db/entity :logseq.property/description) opts)
|
||||
(pv/property-value block property opts))]]])]))))
|
||||
(pv/property-value block' property opts))]]])]))))
|
||||
|
||||
(defn- entity-ref-value?
|
||||
[value]
|
||||
(and (map? value)
|
||||
(or (contains? value :db/id)
|
||||
(contains? value :block/uuid))))
|
||||
|
||||
(defn- contains-recycled-entity-value?
|
||||
[value]
|
||||
(cond
|
||||
(entity-ref-value? value)
|
||||
(ldb/recycled? value)
|
||||
|
||||
(and (coll? value) (not (map? value)))
|
||||
(some (fn [item]
|
||||
(and (entity-ref-value? item)
|
||||
(ldb/recycled? item)))
|
||||
value)
|
||||
|
||||
:else
|
||||
false))
|
||||
|
||||
(defn- filter-recycled-entity-values
|
||||
[value]
|
||||
(let [active-entity-value? (fn [item]
|
||||
(or (not (entity-ref-value? item))
|
||||
(not (ldb/recycled? item))))]
|
||||
(cond
|
||||
(and (entity-ref-value? value) (ldb/recycled? value))
|
||||
nil
|
||||
|
||||
(set? value)
|
||||
(let [value' (set (filter active-entity-value? value))]
|
||||
(when (seq value') value'))
|
||||
|
||||
(vector? value)
|
||||
(let [value' (vec (filter active-entity-value? value))]
|
||||
(when (seq value') value'))
|
||||
|
||||
(and (coll? value) (not (map? value)))
|
||||
(let [value' (vec (filter active-entity-value? value))]
|
||||
(when (seq value') value'))
|
||||
|
||||
:else
|
||||
value)))
|
||||
|
||||
(defn- sanitize-property-values-for-display
|
||||
[properties]
|
||||
(reduce-kv
|
||||
(fn [{:keys [properties recycled-only-property-ids] :as result} property-id property-value]
|
||||
(let [property-value' (filter-recycled-entity-values property-value)]
|
||||
(if (and (nil? property-value')
|
||||
(contains-recycled-entity-value? property-value))
|
||||
(assoc result
|
||||
:properties (assoc properties property-id nil)
|
||||
:recycled-only-property-ids (conj recycled-only-property-ids property-id))
|
||||
(assoc result :properties (assoc properties property-id property-value')))))
|
||||
{:properties {}
|
||||
:recycled-only-property-ids #{}}
|
||||
properties))
|
||||
|
||||
(rum/defc ordered-properties
|
||||
[block properties* sorted-property-entities opts]
|
||||
@@ -603,7 +664,7 @@
|
||||
(let [prev-order (db-order/get-prev-order (db/get-db) nil (:db/id over))]
|
||||
(db-order/gen-key prev-order over-order)))]
|
||||
(db/transact! (state/get-current-repo)
|
||||
[{:db/id (:db/id active)
|
||||
[{:block/uuid (:block/uuid active)
|
||||
:block/order new-order}
|
||||
(outliner-core/block-with-updated-at
|
||||
{:db/id (:db/id block)})]
|
||||
@@ -656,11 +717,13 @@
|
||||
(and show?
|
||||
(or (= mode :global)
|
||||
(and (set? ids) (contains? ids (:block/uuid block))))))
|
||||
properties (cond-> (:block/properties block)
|
||||
(and (ldb/class? block)
|
||||
(not (ldb/built-in? block)))
|
||||
(assoc :logseq.property.class/enable-bidirectional?
|
||||
(:logseq.property.class/enable-bidirectional? block)))
|
||||
properties* (cond-> (:block/properties block)
|
||||
(and (ldb/class? block)
|
||||
(not (ldb/built-in? block)))
|
||||
(assoc :logseq.property.class/enable-bidirectional?
|
||||
(:logseq.property.class/enable-bidirectional? block)))
|
||||
{:keys [properties recycled-only-property-ids]}
|
||||
(sanitize-property-values-for-display properties*)
|
||||
remove-built-in-or-other-position-properties
|
||||
(fn [properties show-in-hidden-properties?]
|
||||
(remove (fn [property]
|
||||
@@ -686,6 +749,7 @@
|
||||
{:keys [all-classes classes-properties]} (outliner-property/get-block-classes-properties (db/get-db) (:db/id block))
|
||||
classes-properties-set (set (map :db/ident classes-properties))
|
||||
block-own-properties (->> properties
|
||||
(remove (fn [[id _]] (contains? recycled-only-property-ids id)))
|
||||
(remove (fn [[id _]] (classes-properties-set id))))
|
||||
state-hide-empty-properties? (:ui/hide-empty-properties? (state/get-config))
|
||||
;; This section produces own-properties and full-hidden-properties
|
||||
@@ -696,7 +760,7 @@
|
||||
show-empty-and-hidden-properties?
|
||||
false
|
||||
state-hide-empty-properties?
|
||||
(nil? (get block property-id))
|
||||
(nil? (get properties property-id))
|
||||
(and (:logseq.property/hide-empty-value property)
|
||||
(nil? (get properties property-id)))
|
||||
true
|
||||
@@ -732,11 +796,15 @@
|
||||
(into result cur-properties)
|
||||
result)))
|
||||
result))
|
||||
class-property-pairs (->> class-properties
|
||||
(map (fn [p] [p (get properties p)]))
|
||||
(remove (fn [[property-id _]]
|
||||
(contains? recycled-only-property-ids property-id))))
|
||||
full-properties (-> (concat block-own-properties'
|
||||
(remove property-hide-f (map (fn [p] [p (get block p)]) class-properties)))
|
||||
(remove property-hide-f class-property-pairs))
|
||||
(remove-built-in-or-other-position-properties false))
|
||||
hidden-properties (-> (concat block-hidden-properties
|
||||
(filter property-hide-f (map (fn [p] [p (get block p)]) class-properties)))
|
||||
(filter property-hide-f class-property-pairs))
|
||||
(remove-built-in-or-other-position-properties true))
|
||||
root-block? (or (= (str (:block/uuid block))
|
||||
(state/get-current-page))
|
||||
|
||||
@@ -36,6 +36,10 @@
|
||||
(when (contains? #{:logseq.property/status :logseq.property/priority} (:db/ident property))
|
||||
(state/pub-event! [:init/commands])))
|
||||
|
||||
(defn- ->block-lookup-id
|
||||
[block]
|
||||
[:block/uuid (:block/uuid block)])
|
||||
|
||||
(defn- <upsert-closed-value!
|
||||
"Create new closed value and returns its block UUID."
|
||||
[property item]
|
||||
@@ -64,7 +68,7 @@
|
||||
(if-let [ent (:logseq.property/description property)]
|
||||
(db/transact! (state/get-current-repo)
|
||||
[(outliner-core/block-with-updated-at
|
||||
{:db/id (:db/id ent) :block/title description})]
|
||||
{:block/uuid (:block/uuid ent) :block/title description})]
|
||||
{:outliner-op :save-block})
|
||||
(when-not (string/blank? description)
|
||||
(db-property-handler/set-block-property!
|
||||
@@ -121,8 +125,11 @@
|
||||
(if (= value :no-tag)
|
||||
(toggle-fn)
|
||||
(p/let [result (<create-class-if-not-exists! value)
|
||||
value' (or result value)
|
||||
tx-data [[(if select? :db/add :db/retract) (:db/id property) :logseq.property/classes [:block/uuid value']]]
|
||||
class-uuid (or result value)
|
||||
tx-data [[(if select? :db/add :db/retract)
|
||||
(->block-lookup-id property)
|
||||
:logseq.property/classes
|
||||
[:block/uuid class-uuid]]]
|
||||
_ (db/transact! (state/get-current-repo) tx-data {:outliner-op :update-property})]
|
||||
(when-not multiple-choices? (toggle-fn)))))}]
|
||||
|
||||
@@ -468,10 +475,10 @@
|
||||
(db-order/gen-key prev-order over-order)))]
|
||||
|
||||
(db/transact! (state/get-current-repo)
|
||||
[{:db/id (:db/id active)
|
||||
[{:block/uuid (:block/uuid active)
|
||||
:block/order new-order}
|
||||
(outliner-core/block-with-updated-at
|
||||
{:db/id (:db/id property)})]
|
||||
{:block/uuid (:block/uuid property)})]
|
||||
{:outliner-op :save-block})))})]
|
||||
(shui/dropdown-menu-separator)])
|
||||
|
||||
|
||||
@@ -313,6 +313,22 @@
|
||||
(when done-choice
|
||||
(db-property/property-value-content done-choice))]])]))
|
||||
|
||||
(defn- <resolve-journal-page-for-date
|
||||
([^js d]
|
||||
(<resolve-journal-page-for-date d
|
||||
state/get-current-repo
|
||||
db-async/<get-block
|
||||
page-handler/<create!
|
||||
date/js-date->journal-title))
|
||||
([^js d get-current-repo-f get-block-f create-page-f journal-title-f]
|
||||
(p/let [journal (journal-title-f d)
|
||||
page (get-block-f (get-current-repo-f) journal {:children? false})
|
||||
journal-page (when (:block/journal-day page)
|
||||
page)]
|
||||
(if journal-page
|
||||
journal-page
|
||||
(create-page-f journal {:redirect? false})))))
|
||||
|
||||
(rum/defcs calendar-inner < rum/reactive db-mixins/query
|
||||
(rum/local (str "calendar-inner-" (js/Date.now)) ::identity)
|
||||
{:init (fn [state]
|
||||
@@ -353,13 +369,8 @@
|
||||
select-handler!
|
||||
(fn [^js d]
|
||||
(when d
|
||||
(p/let [journal (date/js-date->journal-title d)
|
||||
page (db-async/<get-block (state/get-current-repo) journal {:children? false})
|
||||
journal-page (when (:block/journal-day page)
|
||||
page)]
|
||||
(p/let [journal-page (<resolve-journal-page-for-date d)]
|
||||
(p/do!
|
||||
(when-not journal-page
|
||||
(page-handler/<create! journal {:redirect? false}))
|
||||
(when (fn? on-change)
|
||||
(let [value (if datetime? (tc/to-long d) journal-page)]
|
||||
(on-change value)))
|
||||
@@ -1568,6 +1579,23 @@
|
||||
(when (some? value) #{value}))]
|
||||
(multiple-values-inner block property value' opts)))
|
||||
|
||||
(defn- resolved-property-value-for-render
|
||||
[block property multiple-values?]
|
||||
(let [v (get block (:db/ident property))
|
||||
block-loaded? (some? (:block/uuid block))]
|
||||
(or
|
||||
(cond
|
||||
(and multiple-values? (or (set? v) (coll? v) (nil? v)))
|
||||
v
|
||||
multiple-values?
|
||||
#{v}
|
||||
(set? v)
|
||||
(first v)
|
||||
:else
|
||||
v)
|
||||
(when block-loaded?
|
||||
(:logseq.property/default-value property)))))
|
||||
|
||||
(rum/defcs ^:large-vars/cleanup-todo property-value < rum/reactive db-mixins/query
|
||||
[state block property {:keys [show-tooltip? p-block p-property editing?]
|
||||
:as opts}]
|
||||
@@ -1584,18 +1612,7 @@
|
||||
editor-id (str dom-id "-editor")
|
||||
type (:logseq.property/type property)
|
||||
multiple-values? (db-property/many? property)
|
||||
v (let [v (get block (:db/ident property))]
|
||||
(or
|
||||
(cond
|
||||
(and multiple-values? (or (set? v) (coll? v) (nil? v)))
|
||||
v
|
||||
multiple-values?
|
||||
#{v}
|
||||
(set? v)
|
||||
(first v)
|
||||
:else
|
||||
v)
|
||||
(:logseq.property/default-value property)))
|
||||
v (resolved-property-value-for-render block property multiple-values?)
|
||||
self-value-or-embedded? (fn [v]
|
||||
(or (= (:db/id v) (:db/id block))
|
||||
;; property value self embedded
|
||||
|
||||
@@ -4,9 +4,12 @@
|
||||
[datascript.core :as d]
|
||||
[frontend.components.block :as component-block]
|
||||
[frontend.db :as db]
|
||||
[frontend.db-mixins :as db-mixins]
|
||||
[frontend.db.react :as react]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.page :as page-handler]
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.shui.ui :as shui]
|
||||
[rum.core :as rum]))
|
||||
@@ -36,6 +39,20 @@
|
||||
(map #(d/entity db %))
|
||||
(sort-by :logseq.property/deleted-at #(compare %2 %1))))
|
||||
|
||||
(defn- sub-deleted-root-ids
|
||||
[]
|
||||
(when-let [repo (state/get-current-repo)]
|
||||
(some-> (react/q repo
|
||||
[:frontend.worker.react/recycle-roots]
|
||||
{:query-fn (fn [db _]
|
||||
(->> (d/q '[:find [?e ...]
|
||||
:where
|
||||
[?e :logseq.property/deleted-at]]
|
||||
db)
|
||||
vec))}
|
||||
nil)
|
||||
util/react)))
|
||||
|
||||
(defn- group-title
|
||||
[db root]
|
||||
(if (ldb/page? root)
|
||||
@@ -59,7 +76,11 @@
|
||||
(defn- deleted-root-header
|
||||
[db root]
|
||||
(let [user (deleted-by db root)
|
||||
deleted-at (:logseq.property/deleted-at root)]
|
||||
deleted-at (:logseq.property/deleted-at root)
|
||||
root-uuid (:block/uuid root)
|
||||
delete-message (str "Permanently delete this "
|
||||
(if (ldb/page? root) "page" "block")
|
||||
" from Recycle? This cannot be undone.")]
|
||||
[:div.flex.items-center.justify-between.gap-4.text-xs.text-muted-foreground
|
||||
[:div.flex.items-center.gap-1.min-w-0.flex-1
|
||||
(deleted-by-avatar user)
|
||||
@@ -68,12 +89,20 @@
|
||||
(str (if (ldb/page? root) "Page" "Block")
|
||||
" deleted "
|
||||
(.toLocaleString (js/Date. deleted-at)))]]]
|
||||
(shui/button
|
||||
{:variant :ghost
|
||||
:size :xs
|
||||
:class "!py-0 !px-1 h-4"
|
||||
:on-click #(page-handler/restore-recycled! (:block/uuid root))}
|
||||
"Restore")]))
|
||||
[:div.flex.items-center.gap-1
|
||||
(shui/button
|
||||
{:variant :ghost
|
||||
:size :xs
|
||||
:class "!py-0 !px-1 h-4"
|
||||
:on-click #(page-handler/restore-recycled! root-uuid)}
|
||||
"Restore")
|
||||
(shui/button
|
||||
{:variant :ghost
|
||||
:size :xs
|
||||
:class "!py-0 !px-1 h-4 hover:text-red-rx-09 dark:hover:text-red-rx-10 hover:bg-red-rx-04-alpha dark:hover:bg-red-rx-06-alpha"
|
||||
:on-click #(when (js/confirm delete-message)
|
||||
(page-handler/delete-recycled-permanently! root-uuid))}
|
||||
"Delete")]]))
|
||||
|
||||
(defn- deleted-root-outliner
|
||||
[root]
|
||||
@@ -88,16 +117,23 @@
|
||||
:id (str (:block/uuid root))}
|
||||
root))
|
||||
|
||||
(rum/defc recycle-page
|
||||
[_page]
|
||||
(rum/defc recycle-page < rum/reactive db-mixins/query
|
||||
[_page {:keys [class]}]
|
||||
(let [db* (db/get-db)
|
||||
groups (->> (deleted-roots db*)
|
||||
root-ids (or (sub-deleted-root-ids)
|
||||
[])
|
||||
roots (if (seq root-ids)
|
||||
(->> root-ids
|
||||
(keep #(d/entity db* %))
|
||||
(sort-by :logseq.property/deleted-at #(compare %2 %1)))
|
||||
(deleted-roots db*))
|
||||
groups (->> roots
|
||||
(group-by #(group-title db* %))
|
||||
(sort-by (fn [[_ roots]]
|
||||
(:logseq.property/deleted-at (first roots)))
|
||||
#(compare %2 %1)))]
|
||||
[:div.flex.flex-col.gap-1
|
||||
[:div.text-sm.text-muted-foreground.mb-4
|
||||
[:div {:class (util/classnames ["flex" "flex-col" "gap-8" "ls-recycle-page-content" class])}
|
||||
[:div.text-sm.text-muted-foreground.ls-recycle-page-description.ml-1
|
||||
"Deleted pages and blocks stay here until restored or automatically garbage collected after 30 days."]
|
||||
(if (seq groups)
|
||||
(for [[title roots] groups]
|
||||
|
||||
@@ -452,8 +452,8 @@
|
||||
(state/pub-event! [:rtc/sync-app-state])
|
||||
(state/<invoke-db-worker :thread-api/set-db-sync-config
|
||||
{:enabled? true
|
||||
:ws-url config/db-sync-ws-url
|
||||
:http-base config/db-sync-http-base})
|
||||
:ws-url (config/db-sync-ws-url)
|
||||
:http-base (config/db-sync-http-base)})
|
||||
(p/let [rsa-key-pair (state/<invoke-db-worker :thread-api/db-sync-ensure-user-rsa-keys)]
|
||||
(set-e2ee-rsa-key-ensured? (some? rsa-key-pair))))
|
||||
(p/catch (fn [e]
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
[frontend.components.page :as page]
|
||||
[frontend.components.profiler :as profiler]
|
||||
[frontend.components.shortcut-help :as shortcut-help]
|
||||
[frontend.components.plugins :as plugins]
|
||||
[frontend.config :as config]
|
||||
[frontend.context.i18n :refer [t]]
|
||||
[frontend.date :as date]
|
||||
@@ -17,6 +18,7 @@
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.handler.ui :as ui-handler]
|
||||
[frontend.handler.plugin :as plugin-handler]
|
||||
[frontend.state :as state]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.undo-redo.debug-ui :as undo-redo-debug-ui]
|
||||
@@ -134,6 +136,12 @@
|
||||
:page
|
||||
(block-render)
|
||||
|
||||
:plugin
|
||||
[[:.flex.items-center.page-title
|
||||
(ui/icon "puzzle" {:class "text-md mr-2"})
|
||||
[:h3 {:id db-id} (str db-id)]]
|
||||
(plugins/renderer-resolver db-id)]
|
||||
|
||||
:search
|
||||
[[:.flex.items-center.page-title
|
||||
(ui/icon "search" {:class "text-md mr-2"})
|
||||
@@ -423,6 +431,20 @@
|
||||
:tabIndex "0"
|
||||
:data-expanded sidebar-open?}]))
|
||||
|
||||
(rum/defc plugin-renderer-menu-items
|
||||
[renderers]
|
||||
(for [r renderers]
|
||||
(shui/dropdown-menu-item
|
||||
{:on-click #(state/sidebar-add-block!
|
||||
(state/get-current-repo)
|
||||
(keyword (:pid r) (:key r))
|
||||
:plugin
|
||||
)}
|
||||
[:div.flex.items-center
|
||||
{:title (str (:pid r))}
|
||||
[:span.pr-1.flex.items-center (shui/tabler-icon "puzzle")]
|
||||
[:strong (:title r)]])))
|
||||
|
||||
(rum/defcs sidebar-inner <
|
||||
(rum/local false ::anim-finished?)
|
||||
{:will-mount (fn [state]
|
||||
@@ -438,6 +460,20 @@
|
||||
{:on-drag-over util/stop}
|
||||
[:div.cp__right-sidebar-topbar.flex.flex-row.justify-between.items-center
|
||||
[:div.cp__right-sidebar-settings.hide-scrollbar.gap-1 {:key "right-sidebar-settings"}
|
||||
;; sidebar renderers from plugins
|
||||
(when-let [renderers (and config/lsp-enabled?
|
||||
(some->> (plugin-handler/get-hosted-renderers)
|
||||
(filter #(= (:type %) "sidebar"))
|
||||
(seq)))]
|
||||
[:div.text-sm
|
||||
[:button.button.cp__right-sidebar-settings-btn
|
||||
{:on-click (fn [e]
|
||||
(shui/popup-show! e
|
||||
(plugin-renderer-menu-items renderers)
|
||||
{:as-dropdown? true
|
||||
:content-props {:on-click (fn [] (shui/popup-hide!))}}))}
|
||||
[:span.nu.flex.items-center.opacity-80 (shui/tabler-icon "cube-plus")]]])
|
||||
|
||||
[:div.text-sm
|
||||
[:button.button.cp__right-sidebar-settings-btn {:on-click (fn [_e]
|
||||
(state/sidebar-add-block! repo "contents" :contents))}
|
||||
|
||||
@@ -122,6 +122,7 @@
|
||||
[]
|
||||
(let [online? (hooks/use-flow-state flows/network-online-event-flow)
|
||||
[expand-debug? set-expand-debug!] (hooks/use-state false)
|
||||
show-checksums? (or config/dev? util/node-test?)
|
||||
{:keys [graph-uuid local-tx remote-tx local-checksum remote-checksum rtc-state
|
||||
download-logs upload-logs misc-logs pending-local-ops pending-server-ops]}
|
||||
(hooks/use-flow-state (m/watch *detail-info))]
|
||||
@@ -148,8 +149,8 @@
|
||||
graph-uuid (assoc :graph-uuid graph-uuid)
|
||||
local-tx (assoc :local-tx local-tx)
|
||||
remote-tx (assoc :remote-tx remote-tx)
|
||||
local-checksum (assoc :local-checksum local-checksum)
|
||||
remote-checksum (assoc :remote-checksum remote-checksum)
|
||||
(and show-checksums? local-checksum) (assoc :local-checksum local-checksum)
|
||||
(and show-checksums? remote-checksum) (assoc :remote-checksum remote-checksum)
|
||||
rtc-state (assoc :rtc-state rtc-state))
|
||||
pprint/pprint
|
||||
with-out-str)]])
|
||||
|
||||
@@ -76,7 +76,7 @@
|
||||
(if update-pending? (t :settings-page/checking) (t :settings-page/check-for-updates))
|
||||
:class "text-sm mr-1"
|
||||
:disabled update-pending?
|
||||
:on-click #(js/window.apis.checkForUpdates false))
|
||||
:on-click #(js/window.apis.checkForUpdates true))
|
||||
|
||||
:else
|
||||
nil)]
|
||||
@@ -101,6 +101,9 @@
|
||||
(string/blank? type))
|
||||
[:div.update-state.text-sm
|
||||
(case type
|
||||
"checking-for-update"
|
||||
[:p (t :settings-page/checking)]
|
||||
|
||||
"update-not-available"
|
||||
[:p (t :settings-page/app-updated)]
|
||||
|
||||
@@ -114,6 +117,21 @@
|
||||
(util/stop e))}
|
||||
svg/external-link name " 🎉"]])
|
||||
|
||||
"download-progress"
|
||||
(let [percent (some-> payload :percent js/Math.round)]
|
||||
[:p (str "Downloading update"
|
||||
(when (number? percent)
|
||||
(str " " percent "%"))
|
||||
"...")])
|
||||
|
||||
"update-downloaded"
|
||||
[:div.flex.items-center.gap-2.flex-wrap
|
||||
[:p (t :updater/new-version-install)]
|
||||
(ui/button
|
||||
(t :updater/quit-and-install)
|
||||
:class "text-sm"
|
||||
:on-click #(ipc/ipc :quitAndInstall))]
|
||||
|
||||
"error"
|
||||
[:p (t :settings-page/update-error-1) [:br] (t :settings-page/update-error-2)
|
||||
[:a.link
|
||||
@@ -121,7 +139,9 @@
|
||||
(fn [e]
|
||||
(js/window.apis.openExternal "https://github.com/logseq/logseq/releases")
|
||||
(util/stop e))}
|
||||
svg/external-link " release channel"]])])]))
|
||||
svg/external-link " release channel"]]
|
||||
|
||||
nil)])]))
|
||||
|
||||
(rum/defc outdenting-hint
|
||||
[]
|
||||
@@ -547,6 +567,76 @@
|
||||
(config-handler/set-config! :feature/enable-flashcards? value)))
|
||||
true))
|
||||
|
||||
(defn- push-sync-config-to-worker!
|
||||
"Push the current sync URL config to the db worker so changes take effect
|
||||
without restarting the app."
|
||||
[]
|
||||
(state/<invoke-db-worker :thread-api/set-db-sync-config
|
||||
{:enabled? true
|
||||
:ws-url (config/db-sync-ws-url)
|
||||
:http-base (config/db-sync-http-base)}))
|
||||
|
||||
(rum/defc sync-server-url-settings-container
|
||||
[]
|
||||
(let [current-url (config/get-custom-sync-server-url)
|
||||
[url set-url!] (rum/use-state (or current-url ""))
|
||||
reset-url! (fn []
|
||||
(config/set-custom-sync-server-url! nil)
|
||||
(set-url! "")
|
||||
(-> (push-sync-config-to-worker!)
|
||||
(p/then #(notification/show! (t :settings-page/sync-server-url-cleared) :success))
|
||||
(p/catch #(notification/show! (str "Failed to update worker: " %) :error))))]
|
||||
[:div.cp__settings-sync-server-cnt
|
||||
[:h1.mb-2.text-2xl.font-bold (t :settings-page/sync-server-url)]
|
||||
[:div.p-2
|
||||
[:p.text-sm.opacity-70.mb-4 (t :settings-page/sync-server-url-desc)]
|
||||
[:p
|
||||
[:label
|
||||
[:strong "URL"]
|
||||
[:input.form-input.is-small
|
||||
{:value url
|
||||
:placeholder config/default-db-sync-http-base
|
||||
:style {:width "100%"}
|
||||
:on-change #(set-url! (util/evalue %))}]]]
|
||||
[:p.pt-2.flex.gap-2
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:on-click (fn []
|
||||
(let [trimmed (string/trim url)]
|
||||
(if (string/blank? trimmed)
|
||||
(reset-url!)
|
||||
(if-not (config/valid-sync-server-url? trimmed)
|
||||
(notification/show! "URL must start with https:// or http://" :error)
|
||||
(do
|
||||
(config/set-custom-sync-server-url! trimmed)
|
||||
(-> (push-sync-config-to-worker!)
|
||||
(p/then #(notification/show! (t :settings-page/sync-server-url-saved) :success))
|
||||
(p/catch #(notification/show! (str "Failed to update worker: " %) :error))))))))}
|
||||
(t :save))
|
||||
(when (seq url)
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:variant :outline
|
||||
:on-click (fn [] (reset-url!))}
|
||||
(t :settings-page/sync-server-url-reset)))]]]))
|
||||
|
||||
(rum/defc sync-server-url-button
|
||||
[]
|
||||
(let [current-url (config/get-custom-sync-server-url)]
|
||||
(ui/button [:span.flex.items-center
|
||||
[:span.pr-1
|
||||
(if (seq current-url)
|
||||
current-url
|
||||
(t :settings-page/sync-server-url-default))]
|
||||
(ui/icon "edit")]
|
||||
:class "text-sm"
|
||||
:on-click #(state/pub-event! [:go/sync-server-settings]))))
|
||||
|
||||
(defn sync-server-url-row []
|
||||
(row-with-button-action
|
||||
{:left-label (t :settings-page/sync-server-url)
|
||||
:action (sync-server-url-button)}))
|
||||
|
||||
(rum/defc user-proxy-settings
|
||||
[{:keys [type protocol host port] :as agent-opts}]
|
||||
(ui/button [:span.flex.items-center
|
||||
@@ -661,6 +751,7 @@
|
||||
(when (and (or util/mac? util/win32?) (util/electron?)) (app-auto-update-row t))
|
||||
(usage-diagnostics-row t instrument-disabled?)
|
||||
(when-not (mobile-util/native-platform?) (developer-mode-row t developer-mode?))
|
||||
(sync-server-url-row)
|
||||
(when (util/electron?) (https-user-agent-row https-agent-opts))
|
||||
(when (util/electron?) (auto-chmod-row t))
|
||||
;; (clear-cache-row t)
|
||||
@@ -1174,6 +1265,9 @@
|
||||
:did-mount
|
||||
(fn [state]
|
||||
(let [active-tab (first (:rum/args state))
|
||||
active-tab (if (and (= active-tab :ai) (not (util/electron?)))
|
||||
:advanced
|
||||
active-tab)
|
||||
*active (::active state)]
|
||||
(when (keyword? active-tab)
|
||||
(reset! *active [active-tab nil])))
|
||||
@@ -1203,8 +1297,8 @@
|
||||
[:general "general" (t :settings-page/tab-general) (ui/icon "adjustments")]
|
||||
[:editor "editor" (t :settings-page/tab-editor) (ui/icon "writing")]
|
||||
[:keymap "keymap" (t :settings-page/tab-keymap) (ui/icon "keyboard")]
|
||||
|
||||
[:ai (t :settings-page/tab-ai) (t :settings-page/ai) (ui/icon "wand")]
|
||||
(when (util/electron?)
|
||||
[:ai (t :settings-page/tab-ai) (t :settings-page/ai) (ui/icon "wand")])
|
||||
|
||||
[:advanced "advanced" (t :settings-page/tab-advanced) (ui/icon "bulb")]
|
||||
[:features "features" (t :settings-page/tab-features) (ui/icon "app-feature")]
|
||||
@@ -1269,6 +1363,8 @@
|
||||
(encryption)
|
||||
|
||||
:ai
|
||||
(settings-ai)
|
||||
(if (util/electron?)
|
||||
(settings-ai)
|
||||
(settings-advanced))
|
||||
|
||||
nil)]]]))
|
||||
|
||||
@@ -133,6 +133,16 @@
|
||||
|
||||
(defonce *last-header-action-target (atom nil))
|
||||
|
||||
(defn- header-dropdown-click-should-hide?
|
||||
[target]
|
||||
(let [menu-item (some-> target (.closest "[role='menuitem']"))
|
||||
submenu-trigger? (= "menu" (some-> menu-item (.getAttribute "aria-haspopup")))]
|
||||
(boolean
|
||||
(and target
|
||||
(not (util/input? target))
|
||||
menu-item
|
||||
(not submenu-trigger?)))))
|
||||
|
||||
(defn header-cp
|
||||
[{:keys [view-entity column-set-sorting! state]} column]
|
||||
(let [sorting (:sorting state)
|
||||
@@ -194,6 +204,10 @@
|
||||
:align "start"
|
||||
:as-dropdown? true
|
||||
:dropdown-menu? true
|
||||
:content-props {:on-click (fn [^js e]
|
||||
(when-let [target (.-target e)]
|
||||
(when (header-dropdown-click-should-hide? target)
|
||||
(shui/popup-hide! popup-id))))}
|
||||
:on-before-hide (fn []
|
||||
(reset! *last-header-action-target el)
|
||||
(js/setTimeout #(reset! *last-header-action-target nil) 128))})))))}
|
||||
@@ -1863,7 +1877,8 @@
|
||||
(cond->
|
||||
{:page (:block/uuid page)
|
||||
:properties properties
|
||||
:edit-block? false}
|
||||
:edit-block? false
|
||||
:outliner-op :create-view}
|
||||
auto-triggered?
|
||||
(assoc :custom-uuid view-block-id)))]
|
||||
(db/entity [:block/uuid (:block/uuid result)]))))
|
||||
|
||||
@@ -49,20 +49,71 @@
|
||||
(goog-define ENABLE-DB-SYNC-LOCAL false)
|
||||
(defonce db-sync-local? ENABLE-DB-SYNC-LOCAL)
|
||||
|
||||
(defonce db-sync-ws-url
|
||||
(defonce default-db-sync-ws-url
|
||||
(if db-sync-local?
|
||||
"ws://127.0.0.1:8787/sync/%s"
|
||||
"wss://api-staging.logseq.io/sync/%s"
|
||||
;; "wss://api-staging.logseq.io/sync/%s"
|
||||
))
|
||||
|
||||
(defonce db-sync-http-base
|
||||
(defonce default-db-sync-http-base
|
||||
(if db-sync-local?
|
||||
"http://127.0.0.1:8787"
|
||||
"https://api-staging.logseq.io"
|
||||
;; "https://api-staging.logseq.io"
|
||||
))
|
||||
|
||||
(defn get-custom-sync-server-url
|
||||
"Read the user-configured custom sync server URL from localStorage.
|
||||
Returns nil when not set or empty."
|
||||
[]
|
||||
(when-not util/node-test?
|
||||
(let [v (.getItem js/localStorage "sync-server-url")]
|
||||
(when (and (string? v) (not (string/blank? v)))
|
||||
v))))
|
||||
|
||||
(defn set-custom-sync-server-url!
|
||||
"Persist the custom sync server URL to localStorage. Pass nil or empty string to clear."
|
||||
[url]
|
||||
(when-not util/node-test?
|
||||
(if (or (nil? url) (string/blank? url))
|
||||
(.removeItem js/localStorage "sync-server-url")
|
||||
(.setItem js/localStorage "sync-server-url" (string/trim url)))))
|
||||
|
||||
(defn valid-sync-server-url?
|
||||
"Return true when `url` looks like a valid HTTP(S) base URL."
|
||||
[url]
|
||||
(and (string? url)
|
||||
(re-find #"^https?://" url)))
|
||||
|
||||
(defn custom-url->ws-url
|
||||
"Derive a WebSocket sync URL from a custom HTTP base URL. Pure function."
|
||||
[custom-url]
|
||||
(let [scheme (if (string/starts-with? custom-url "https") "wss" "ws")
|
||||
base (-> custom-url
|
||||
(string/replace #"^https?://" "")
|
||||
(string/replace #"/+$" ""))]
|
||||
(str scheme "://" base "/sync/%s")))
|
||||
|
||||
(defn custom-url->http-base
|
||||
"Normalize a custom HTTP base URL by stripping trailing slashes. Pure function."
|
||||
[custom-url]
|
||||
(string/replace custom-url #"/+$" ""))
|
||||
|
||||
(defn db-sync-ws-url
|
||||
"Return the WebSocket sync URL. Uses custom server when configured, otherwise the default."
|
||||
[]
|
||||
(if-let [custom (get-custom-sync-server-url)]
|
||||
(custom-url->ws-url custom)
|
||||
default-db-sync-ws-url))
|
||||
|
||||
(defn db-sync-http-base
|
||||
"Return the HTTP base URL for sync. Uses custom server when configured, otherwise the default."
|
||||
[]
|
||||
(if-let [custom (get-custom-sync-server-url)]
|
||||
(custom-url->http-base custom)
|
||||
default-db-sync-http-base))
|
||||
|
||||
;; Feature flags
|
||||
;; =============
|
||||
|
||||
|
||||
@@ -51,6 +51,87 @@
|
||||
(state/<invoke-db-worker :thread-api/get-bidirectional-properties (state/get-current-repo)
|
||||
{:target-id target-id})))
|
||||
|
||||
(defn- worker-get-blocks-requests
|
||||
[requests]
|
||||
(mapv (fn [{:keys [id opts]}]
|
||||
{:id id
|
||||
:opts (select-keys opts [:children? :properties :include-collapsed-children?])})
|
||||
requests))
|
||||
|
||||
(defn- <invoke-worker-get-blocks
|
||||
[graph requests]
|
||||
(p/let [result-transit-str
|
||||
(state/<invoke-db-worker-direct-pass :thread-api/get-blocks
|
||||
graph
|
||||
(ldb/write-transit-str requests))]
|
||||
(some-> result-transit-str ldb/read-transit-str)))
|
||||
|
||||
(defonce ^:private *get-blocks-batch-enabled? (atom true))
|
||||
|
||||
(defonce ^:private *get-blocks-batch-state
|
||||
(atom {:scheduled? false
|
||||
:queue []}))
|
||||
|
||||
(declare flush-get-blocks-batch!)
|
||||
|
||||
(defn- schedule-get-blocks-batch-flush!
|
||||
[]
|
||||
(let [should-schedule? (not (:scheduled? @*get-blocks-batch-state))]
|
||||
(when should-schedule?
|
||||
(swap! *get-blocks-batch-state assoc :scheduled? true)
|
||||
(util/schedule flush-get-blocks-batch!))))
|
||||
|
||||
(defn- enqueue-get-blocks-request!
|
||||
[graph request]
|
||||
(let [result (p/deferred)]
|
||||
(swap! *get-blocks-batch-state
|
||||
(fn [state]
|
||||
(update state :queue conj {:graph graph
|
||||
:request request
|
||||
:result result})))
|
||||
(schedule-get-blocks-batch-flush!)
|
||||
result))
|
||||
|
||||
(defn- resolve-batched-get-blocks!
|
||||
[entries responses]
|
||||
(doseq [[idx {:keys [result]}] (map-indexed vector entries)]
|
||||
(p/resolve! result (nth responses idx nil))))
|
||||
|
||||
(defn- reject-batched-get-blocks!
|
||||
[entries error]
|
||||
(doseq [{:keys [result]} entries]
|
||||
(p/reject! result error)))
|
||||
|
||||
(defn- flush-get-blocks-batch!
|
||||
[]
|
||||
(let [queue (:queue @*get-blocks-batch-state)]
|
||||
(swap! *get-blocks-batch-state
|
||||
(fn [state] (assoc state :scheduled? false :queue [])))
|
||||
(doseq [[graph entries] (group-by :graph queue)]
|
||||
(let [requests (->> entries (map :request) worker-get-blocks-requests)]
|
||||
(->
|
||||
(p/let [result (<invoke-worker-get-blocks graph requests)
|
||||
result (if (= (count result) (count requests))
|
||||
result
|
||||
nil)
|
||||
result (or result
|
||||
;; Safety fallback: retry once if response length is unexpected.
|
||||
(<invoke-worker-get-blocks graph requests))]
|
||||
(resolve-batched-get-blocks! entries result))
|
||||
(p/catch (fn [error]
|
||||
(reject-batched-get-blocks! entries error))))))))
|
||||
|
||||
(defn- <fetch-blocks-from-worker-batched
|
||||
[graph requests]
|
||||
(when (seq requests)
|
||||
(if @*get-blocks-batch-enabled?
|
||||
(-> (p/all (mapv #(enqueue-get-blocks-request! graph %) requests))
|
||||
(p/catch (fn [_]
|
||||
;; Fail-open: disable batching for this runtime and fall back to direct fetch.
|
||||
(reset! *get-blocks-batch-enabled? false)
|
||||
(<invoke-worker-get-blocks graph (worker-get-blocks-requests requests)))))
|
||||
(<invoke-worker-get-blocks graph (worker-get-blocks-requests requests)))))
|
||||
|
||||
(defn <get-block
|
||||
[graph id-uuid-or-name & {:keys [children? include-collapsed-children? skip-transact? skip-refresh? properties]
|
||||
:or {children? true}
|
||||
@@ -80,9 +161,7 @@
|
||||
|
||||
:else
|
||||
(->
|
||||
(p/let [result-transit-str (state/<invoke-db-worker-direct-pass :thread-api/get-blocks graph
|
||||
(ldb/write-transit-str [{:id id :opts opts}]))
|
||||
result (ldb/read-transit-str result-transit-str)
|
||||
(p/let [result (<fetch-blocks-from-worker-batched graph [{:id id :opts opts}])
|
||||
{:keys [block children]} (first result)]
|
||||
(when-not skip-transact?
|
||||
(let [conn (db/get-db graph false)
|
||||
@@ -110,19 +189,15 @@
|
||||
[graph ids* & {:as opts}]
|
||||
(let [ids (remove (fn [id] (:block.temp/load-status (db/entity id))) ids*)]
|
||||
(when (seq ids)
|
||||
(p/let [result-transit-str
|
||||
(state/<invoke-db-worker-direct-pass :thread-api/get-blocks graph
|
||||
(ldb/write-transit-str
|
||||
(map
|
||||
(fn [id]
|
||||
{:id id :opts (assoc opts :children? false)})
|
||||
ids)))
|
||||
result (ldb/read-transit-str result-transit-str)]
|
||||
(p/let [result (<fetch-blocks-from-worker-batched graph
|
||||
(mapv (fn [id]
|
||||
{:id id :opts (assoc opts :children? false)})
|
||||
ids))]
|
||||
(let [conn (db/get-db graph false)
|
||||
result' (map :block result)]
|
||||
result' (keep :block result)]
|
||||
(when (seq result')
|
||||
(d/transact! conn result'))
|
||||
result')))))
|
||||
result)))))
|
||||
|
||||
(defn <get-block-parents
|
||||
[graph id depth]
|
||||
|
||||
@@ -114,12 +114,11 @@
|
||||
|
||||
(defn- <create-cards-block!
|
||||
[]
|
||||
(let [cards-tag-id (:db/id (db/entity :logseq.class/Cards))]
|
||||
(editor-handler/api-insert-new-block! ""
|
||||
{:page (date/today)
|
||||
:properties {:block/tags #{cards-tag-id}}
|
||||
:sibling? false
|
||||
:end? true})))
|
||||
(editor-handler/api-insert-new-block! ""
|
||||
{:page (date/today)
|
||||
:properties {:block/tags #{:logseq.class/Cards}}
|
||||
:sibling? false
|
||||
:end? true}))
|
||||
|
||||
(defn- btn-with-shortcut [{:keys [shortcut id btn-text due on-click class]}]
|
||||
(let [bg-class (case id
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
(:db/id color))) colors)]
|
||||
(when color-id
|
||||
(let [properties (cond->
|
||||
{:block/tags #{(:db/id (db/entity :logseq.class/Pdf-annotation))}
|
||||
{:block/tags #{:logseq.class/Pdf-annotation}
|
||||
:block/collapsed? image?
|
||||
:logseq.property/ls-type :annotation
|
||||
:logseq.property.pdf/hl-color color-id
|
||||
|
||||
@@ -250,7 +250,7 @@
|
||||
(p/do!
|
||||
(ui-outliner-tx/transact!
|
||||
{:outliner-op :move-blocks
|
||||
:real-outliner-op :indent-outdent}
|
||||
:source-outliner-op :indent-outdent}
|
||||
(when save-current-block (save-current-block))
|
||||
(outliner-op/indent-outdent-blocks! (get-top-level-blocks blocks')
|
||||
indent?
|
||||
|
||||
@@ -95,11 +95,30 @@
|
||||
(string/replace #"[\\/]+" "_")
|
||||
(str "_checksum_" (quot (util/time-ms) 1000))))
|
||||
|
||||
(defn- client-server-checksum-mismatch?
|
||||
[local-checksum remote-checksum]
|
||||
(and (string? local-checksum)
|
||||
(string? remote-checksum)
|
||||
(not= local-checksum remote-checksum)))
|
||||
(defn- client-ops-export-file-name
|
||||
[repo]
|
||||
(-> (or repo "graph")
|
||||
(string/replace #"^/+" "")
|
||||
(string/replace #"[\\/]+" "_")
|
||||
(str "_client_ops_" (quot (util/time-ms) 1000))))
|
||||
|
||||
(defn- ->uint8array
|
||||
[data]
|
||||
(cond
|
||||
(instance? js/Uint8Array data)
|
||||
data
|
||||
|
||||
(js/ArrayBuffer.isView data)
|
||||
(js/Uint8Array. (.-buffer data) (.-byteOffset data) (.-byteLength data))
|
||||
|
||||
(instance? js/ArrayBuffer data)
|
||||
(js/Uint8Array. data)
|
||||
|
||||
(array? data)
|
||||
(js/Uint8Array. data)
|
||||
|
||||
:else
|
||||
nil))
|
||||
|
||||
(defn- <fetch-server-checksum-diagnostics
|
||||
[repo]
|
||||
@@ -164,7 +183,9 @@
|
||||
:server-checksum (:checksum server-diagnostics)
|
||||
:different-blocks diff-blocks}]
|
||||
(pprint/pprint diff-data)
|
||||
(js/console.warn "Checksum mismatch between client and server. Diff data:" diff-data)))
|
||||
(when (seq diff-blocks)
|
||||
(js/console.warn "Checksum mismatch between client and server. Diff data:" diff-data))))
|
||||
|
||||
|
||||
(defn ^:export recompute-checksum-diagnostics
|
||||
[]
|
||||
@@ -184,11 +205,10 @@
|
||||
content (with-out-str (pprint/pprint export-edn))
|
||||
blob (js/Blob. #js [content] (clj->js {:type "text/edn;charset=utf-8"}))
|
||||
filename (checksum-export-file-name repo)]
|
||||
(p/let [_ (when (client-server-checksum-mismatch? local-checksum remote-checksum)
|
||||
(-> (<log-checksum-mismatch-diff! repo export-edn)
|
||||
(p/catch (fn [error]
|
||||
(js/console.error "checksum mismatch diff fetch failed:" error)
|
||||
nil))))]
|
||||
(p/let [_ (-> (<log-checksum-mismatch-diff! repo export-edn)
|
||||
(p/catch (fn [error]
|
||||
(js/console.error "checksum mismatch diff fetch failed:" error)
|
||||
nil)))]
|
||||
(utils/saveToFile blob filename "edn")
|
||||
(notification/show!
|
||||
(str "Checksum recomputed. Recomputed: " recomputed-checksum
|
||||
@@ -204,6 +224,29 @@
|
||||
(notification/show! "Failed to compute graph checksum diagnostics." :error))))
|
||||
(notification/show! "No graph found" :warning)))
|
||||
|
||||
(defn ^:export export-client-ops-sqlite
|
||||
[]
|
||||
(if-let [repo (state/get-current-repo)]
|
||||
(-> (state/<invoke-db-worker-direct-pass :thread-api/export-client-ops-db repo)
|
||||
(p/then (fn [data]
|
||||
(if-let [payload (->uint8array data)]
|
||||
(let [filename (client-ops-export-file-name repo)
|
||||
blob (js/Blob. #js [payload] (clj->js {:type "application/octet-stream"}))]
|
||||
(utils/saveToFile blob filename "sqlite")
|
||||
(notification/show!
|
||||
(str "Client ops SQLite exported: " filename ".sqlite")
|
||||
:success
|
||||
false))
|
||||
(notification/show!
|
||||
(str "Client ops SQLite export failed: invalid payload type "
|
||||
(pr-str (type data))
|
||||
".")
|
||||
:warning))))
|
||||
(p/catch (fn [error]
|
||||
(js/console.error "export-client-ops-sqlite failed:" error)
|
||||
(notification/show! "Failed to export client ops SQLite." :error))))
|
||||
(notification/show! "No graph found" :warning)))
|
||||
|
||||
(defn import-chosen-graph
|
||||
[repo]
|
||||
(p/let [_ (persist-db/<close-db repo)]
|
||||
|
||||
@@ -36,6 +36,22 @@
|
||||
(rest parts)))
|
||||
(string/join " #"))))
|
||||
|
||||
(defn- find-page-add-button
|
||||
[page-id]
|
||||
(when page-id
|
||||
(->> (dom/sel ".block-add-button")
|
||||
(filter #(= (str page-id) (dom/attr % "parentblockid")))
|
||||
first)))
|
||||
|
||||
(defn- click-page-add-button-with-retry!
|
||||
[page-id]
|
||||
(letfn [(poll! [remaining-ms]
|
||||
(if-let [block-add-button (find-page-add-button page-id)]
|
||||
(.click block-add-button)
|
||||
(when (pos? remaining-ms)
|
||||
(js/setTimeout #(poll! (- remaining-ms 100)) 100))))]
|
||||
(poll! 500)))
|
||||
|
||||
(defn <create!
|
||||
([title]
|
||||
(<create! title {}))
|
||||
@@ -67,7 +83,7 @@
|
||||
:else
|
||||
(when-not (string/blank? page-title)
|
||||
(p/let [existing-page (when-not class? (db/get-page page-title))]
|
||||
(if existing-page
|
||||
(if (and existing-page (not (ldb/recycled? existing-page)))
|
||||
existing-page
|
||||
(p/let [options' (cond-> (update options :tags concat (:block/tags parsed-result))
|
||||
(nil? (:split-namespace? options))
|
||||
@@ -79,13 +95,7 @@
|
||||
(when redirect?
|
||||
(route-handler/redirect-to-page! page-uuid)
|
||||
(when-not today-journal?
|
||||
(js/setTimeout
|
||||
(fn []
|
||||
(when-let [block-add-button (->> (dom/sel ".block-add-button")
|
||||
(filter #(= (str (:db/id page)) (dom/attr % "parentblockid")))
|
||||
first)]
|
||||
(.click block-add-button)))
|
||||
200)))
|
||||
(click-page-add-button-with-retry! (:db/id page))))
|
||||
page)))))))))
|
||||
|
||||
;; favorite fns
|
||||
@@ -147,8 +157,8 @@
|
||||
(config-handler/set-config! :feature/enable-journals? true)
|
||||
(notification/show! "Journals enabled" :success)))
|
||||
(-> (p/let [res (ui-outliner-tx/transact!
|
||||
{:outliner-op :delete-page}
|
||||
(outliner-op/delete-page! page-uuid))]
|
||||
{:outliner-op :delete-page}
|
||||
(outliner-op/delete-page! page-uuid))]
|
||||
(if res
|
||||
(when ok-handler (ok-handler))
|
||||
(when error-handler (error-handler))))
|
||||
|
||||
@@ -48,7 +48,9 @@
|
||||
|
||||
(defn wrap-parse-block
|
||||
[{:block/keys [title level] :as block}]
|
||||
(let [block (or (and (:db/id block) (db/entity (:db/id block))) block)
|
||||
(let [block (or (and (:db/id block) (db/entity (:db/id block)))
|
||||
(and (:block/uuid block) (db/entity [:block/uuid (:block/uuid block)]))
|
||||
block)
|
||||
block (if (nil? title)
|
||||
block
|
||||
(let [ast (mldoc/->edn (string/trim title) :markdown)
|
||||
|
||||
@@ -52,8 +52,9 @@
|
||||
(ldb/built-in? page-entity)
|
||||
(notification/show! "Built-in pages can't be used as tags" :error)
|
||||
:else
|
||||
;; FIXME: should move to worker
|
||||
(let [txs [(db-class/build-new-class (db/get-db)
|
||||
{:db/id (:db/id page-entity)
|
||||
{:block/uuid (:block/uuid page-entity)
|
||||
:block/title (:block/title page-entity)
|
||||
:block/created-at (:block/created-at page-entity)})
|
||||
[:db/retract (:db/id page-entity) :block/tags :logseq.class/Page]]]
|
||||
|
||||
@@ -28,8 +28,8 @@
|
||||
base)))
|
||||
|
||||
(defn http-base []
|
||||
(or config/db-sync-http-base
|
||||
(ws->http-base config/db-sync-ws-url)))
|
||||
(or (config/db-sync-http-base)
|
||||
(ws->http-base (config/db-sync-ws-url))))
|
||||
|
||||
(defn- auth-headers []
|
||||
(when-let [token (state/get-auth-id-token)]
|
||||
|
||||
@@ -250,8 +250,7 @@
|
||||
|
||||
(defn- save-block-inner!
|
||||
[block value opts]
|
||||
(let [block {:db/id (:db/id block)
|
||||
:block/uuid (:block/uuid block)
|
||||
(let [block {:block/uuid (:block/uuid block)
|
||||
:block/title value}
|
||||
block' (-> (wrap-parse-block block)
|
||||
;; :block/uuid might be changed when backspace/delete
|
||||
@@ -320,7 +319,6 @@
|
||||
{:outliner-op :insert-blocks}
|
||||
(save-current-block! {:current-block current-block})
|
||||
(outliner-op/insert-blocks! [new-block'] current-block {:sibling? sibling?
|
||||
:right-sibling-id (:db/id (:right-sibling config))
|
||||
:keep-uuid? keep-uuid?
|
||||
:ordered-list? ordered-list?
|
||||
:replace-empty-target? replace-empty-target?
|
||||
@@ -595,12 +593,14 @@
|
||||
(into new-block properties)
|
||||
new-block)]
|
||||
(ui-outliner-tx/transact!
|
||||
{:outliner-op :insert-blocks}
|
||||
(cond->
|
||||
{:outliner-op :insert-blocks}
|
||||
(not= outliner-op :insert-blocks)
|
||||
(assoc :source-outliner-op outliner-op))
|
||||
(outliner-insert-block! config target-block new-block'
|
||||
{:sibling? sibling?
|
||||
:keep-uuid? true
|
||||
:ordered-list? ordered-list?
|
||||
:outliner-op outliner-op
|
||||
:replace-empty-target? replace-empty-target?})))
|
||||
(when edit-block?
|
||||
(if (and replace-empty-target?
|
||||
@@ -1083,8 +1083,6 @@
|
||||
(:db/id page)
|
||||
:page))))))
|
||||
|
||||
(declare save-current-block!)
|
||||
|
||||
;; FIXME: shortcut `mod+.` doesn't work on Web (Chrome)
|
||||
(defn zoom-in! []
|
||||
(if (state/editing?)
|
||||
@@ -1348,8 +1346,7 @@
|
||||
(notification/show! [:div "Asset size shouldn't be larger than 100M"]
|
||||
:warning
|
||||
false)
|
||||
(throw (ex-info "Asset size shouldn't be larger than 100M" {:file-name file-name})))
|
||||
asset (db/entity :logseq.class/Asset)]
|
||||
(throw (ex-info "Asset size shouldn't be larger than 100M" {:file-name file-name})))]
|
||||
(p/do!
|
||||
(when file
|
||||
(let [file-path (str block-id "." ext)]
|
||||
@@ -1360,7 +1357,9 @@
|
||||
:logseq.property.asset/external-url external-url
|
||||
:logseq.property.asset/size size
|
||||
:logseq.property.asset/checksum checksum
|
||||
:block/tags #{(:db/id asset)}})))))
|
||||
;; Use stable class ident in tx payload to avoid leaking numeric eids
|
||||
;; into outliner history ops shared with the worker sync pipeline.
|
||||
:block/tags #{:logseq.class/Asset}})))))
|
||||
|
||||
(defn db-based-save-assets!
|
||||
"Save incoming(pasted) assets to assets directory.
|
||||
@@ -1821,7 +1820,7 @@
|
||||
(content-update-fn (:block/title block))
|
||||
(:block/title block))]
|
||||
(merge (apply dissoc block (conj (if-not keep-uuid? [:block/_refs] [])))
|
||||
{:block/page {:db/id (:db/id page)}
|
||||
{:block/page {:db/id [:block/uuid (:block/uuid page)]}
|
||||
:block/title new-content})))
|
||||
|
||||
(defn- edit-last-block-after-inserted!
|
||||
@@ -1840,11 +1839,12 @@
|
||||
|
||||
(defn- unrecycle-tx-data
|
||||
[root]
|
||||
[[:db/retract (:db/id root) :logseq.property/deleted-at]
|
||||
[:db/retract (:db/id root) :logseq.property/deleted-by-ref]
|
||||
[:db/retract (:db/id root) :logseq.property.recycle/original-parent]
|
||||
[:db/retract (:db/id root) :logseq.property.recycle/original-page]
|
||||
[:db/retract (:db/id root) :logseq.property.recycle/original-order]])
|
||||
(let [root-id [:block/uuid (:block/uuid root)]]
|
||||
[[:db/retract root-id :logseq.property/deleted-at]
|
||||
[:db/retract root-id :logseq.property/deleted-by-ref]
|
||||
[:db/retract root-id :logseq.property.recycle/original-parent]
|
||||
[:db/retract root-id :logseq.property.recycle/original-page]
|
||||
[:db/retract root-id :logseq.property.recycle/original-order]]))
|
||||
|
||||
(defn paste-blocks
|
||||
"Given a vec of blocks, insert them into the target page.
|
||||
|
||||
@@ -62,6 +62,12 @@
|
||||
[error]
|
||||
(string/includes? (or (ex-message error) (str error)) "decrypt-aes-key"))
|
||||
|
||||
(defn- <build-search-index!
|
||||
[repo]
|
||||
(-> (state/<invoke-db-worker :thread-api/search-build-blocks-indice-in-worker repo)
|
||||
(p/catch (fn [error]
|
||||
(js/console.error "Search index build error:" error)))))
|
||||
|
||||
(defn- schedule-search-index-build!
|
||||
[repo]
|
||||
(when-let [timeout-id @*search-index-build-timeout]
|
||||
@@ -76,9 +82,7 @@
|
||||
(state/input-idle? repo :diff 5000)
|
||||
(do
|
||||
(reset! *search-index-build-timeout nil)
|
||||
(-> (state/<invoke-db-worker :thread-api/search-build-blocks-indice-in-worker repo)
|
||||
(p/catch (fn [error]
|
||||
(js/console.error "Search index build error:" error)))))
|
||||
(<build-search-index! repo))
|
||||
|
||||
:else
|
||||
(schedule-search-index-build! repo)))
|
||||
@@ -110,7 +114,7 @@
|
||||
(p/do!
|
||||
(p/delay 5000)
|
||||
(p/let [repo (state/get-current-repo)
|
||||
_ (state/<invoke-db-worker :thread-api/search-build-blocks-indice-in-worker repo)]
|
||||
_ (<build-search-index! repo)]
|
||||
(when state/lsp-enabled?
|
||||
(doseq [service (state/get-all-plugin-services-with-type :search)]
|
||||
(search-plugin/call-service! service "search:rebuildPagesIndice" {})
|
||||
|
||||
@@ -81,6 +81,11 @@
|
||||
(plugin/user-proxy-settings-container agent-opts)
|
||||
{:id :https-proxy-panel :center? true :class "lg:max-w-2xl"}))
|
||||
|
||||
(defmethod events/handle :go/sync-server-settings [[_]]
|
||||
(shui/dialog-open!
|
||||
(settings/sync-server-url-settings-container)
|
||||
{:id :sync-server-panel :center? true :class "lg:max-w-2xl"}))
|
||||
|
||||
(defmethod events/handle :redirect-to-home [_]
|
||||
(page-handler/create-today-journal!)
|
||||
(when (util/capacitor?)
|
||||
@@ -300,8 +305,7 @@
|
||||
{:id :selection-action-bar
|
||||
:root-props {:modal false}
|
||||
:content-props {:side "top"
|
||||
:class "!py-0 !px-0 !border-none"
|
||||
:modal? false}
|
||||
:class "!py-0 !px-0 !border-none"}
|
||||
:auto-side? false
|
||||
:align :start}))))
|
||||
|
||||
@@ -331,8 +335,8 @@
|
||||
(state/pub-event! [:rtc/sync-app-state])
|
||||
(state/<invoke-db-worker :thread-api/set-db-sync-config
|
||||
{:enabled? true
|
||||
:ws-url config/db-sync-ws-url
|
||||
:http-base config/db-sync-http-base})
|
||||
:ws-url (config/db-sync-ws-url)
|
||||
:http-base (config/db-sync-http-base)})
|
||||
(state/<invoke-db-worker :thread-api/db-sync-ensure-user-rsa-keys))
|
||||
(p/catch (fn [error]
|
||||
(log/error :db-sync/ensure-user-rsa-keys-failed error)
|
||||
|
||||
@@ -57,6 +57,16 @@
|
||||
(outliner-op/transact! tx-data nil))
|
||||
true))))
|
||||
|
||||
(defn delete-recycled-permanently!
|
||||
[root-uuid]
|
||||
(when-let [root (db/entity [:block/uuid root-uuid])]
|
||||
(when (seq (outliner-recycle/permanently-delete-tx-data (db/get-db) root))
|
||||
(p/do!
|
||||
(ui-outliner-tx/transact!
|
||||
{:outliner-op :recycle-delete-permanently}
|
||||
(outliner-op/recycle-delete-permanently! root-uuid))
|
||||
true))))
|
||||
|
||||
(defn <unfavorite-page!
|
||||
[page-name]
|
||||
(p/do!
|
||||
@@ -125,7 +135,7 @@
|
||||
|
||||
(defn update-public-attribute!
|
||||
[page value]
|
||||
(db-property-handler/set-block-property! [:block/uuid (:block/uuid page)] :logseq.property/publishing-public? value))
|
||||
(db-property-handler/set-block-property! (:block/uuid page) :logseq.property/publishing-public? value))
|
||||
|
||||
(defn get-page-ref-text
|
||||
[page]
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
[frontend.fs :as fs]
|
||||
[frontend.handler.common.plugin :as plugin-common-handler]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.handler.plugin-config :as plugin-config-handler]
|
||||
[frontend.idb :as idb]
|
||||
[frontend.modules.shortcut.utils :as shortcut-utils]
|
||||
[frontend.state :as state]
|
||||
@@ -35,6 +36,13 @@
|
||||
(uuid? x) (str x)
|
||||
:else x)) input))))
|
||||
|
||||
(defn- normalize-user-key-without-ns
|
||||
[k]
|
||||
(some-> k (name)
|
||||
(string/replace "/" "$")
|
||||
(string/replace " " "_")
|
||||
(string/replace #"^[:_\s]+" "")))
|
||||
|
||||
(defn invoke-exported-api
|
||||
[type & args]
|
||||
(try
|
||||
@@ -53,7 +61,99 @@
|
||||
(defonce central-endpoint "https://raw.githubusercontent.com/logseq/marketplace/master/")
|
||||
(defonce plugins-url (str central-endpoint "plugins.json"))
|
||||
(defonce stats-url (str central-endpoint "stats.json"))
|
||||
(declare select-a-plugin-theme)
|
||||
(declare select-a-plugin-theme get-ls-dotdir-root)
|
||||
|
||||
(def ^:private illegal-plugin-package-error-pattern
|
||||
#"^Parse package config error #(.+)[/\\]package\.json$")
|
||||
|
||||
(defn- illegal-plugin-package-error->data
|
||||
[^js e]
|
||||
(let [name (some-> e (aget "name"))
|
||||
message (some-> e (aget "message"))
|
||||
url (or (some-> e (aget "url"))
|
||||
(some->> message (re-matches illegal-plugin-package-error-pattern) second))
|
||||
url (some-> url util/node-path.normalize)
|
||||
package-json-path (or (some-> e (aget "packageJsonPath"))
|
||||
(some-> url (util/node-path.join "package.json")))]
|
||||
(when (and (= "IllegalPluginPackageError" name)
|
||||
(not (string/blank? url)))
|
||||
{:url url
|
||||
:package-json-path package-json-path})))
|
||||
|
||||
(defn- problematic-plugin-source
|
||||
[url]
|
||||
(let [url (util/node-path.normalize url)
|
||||
dotroot (some-> (get-ls-dotdir-root) util/node-path.normalize)
|
||||
dotplugins-root (some-> dotroot (util/node-path.join "plugins"))]
|
||||
(if (and dotplugins-root
|
||||
(string/starts-with? url dotplugins-root))
|
||||
{:type :installed
|
||||
:id (util/node-path.basename url)
|
||||
:url url}
|
||||
{:type :external
|
||||
:url url})))
|
||||
|
||||
(defn- remove-external-plugin-path!
|
||||
[url]
|
||||
(p/let [prefs (invoke-exported-api :load_user_preferences)
|
||||
prefs (if prefs (js->clj prefs :keywordize-keys true) {})
|
||||
url (util/node-path.normalize url)
|
||||
externals (mapv util/node-path.normalize (or (:externals prefs) []))
|
||||
updated-externals (vec (remove #(= % url) externals))
|
||||
removed? (not= externals updated-externals)
|
||||
_ (when removed?
|
||||
(invoke-exported-api :save_user_preferences
|
||||
(clj->js (assoc prefs :externals updated-externals))))]
|
||||
removed?))
|
||||
|
||||
(defn- remove-problematic-plugin!
|
||||
[url]
|
||||
(let [{:keys [type id] :as source} (problematic-plugin-source url)]
|
||||
(case type
|
||||
:installed
|
||||
(p/let [_ (when (util/electron?)
|
||||
(ipc/ipc :uninstallMarketPlugin id))
|
||||
_ (-> (plugin-config-handler/remove-plugin id)
|
||||
(p/catch (fn [error]
|
||||
(log/warn :remove-broken-plugin-config-error error))))]
|
||||
source)
|
||||
|
||||
:external
|
||||
(p/let [removed? (remove-external-plugin-path! url)]
|
||||
(assoc source :removed? removed?)))))
|
||||
|
||||
(defn show-illegal-plugin-package-notification!
|
||||
[^js e]
|
||||
(if-let [{:keys [url package-json-path]} (illegal-plugin-package-error->data e)]
|
||||
(let [{:keys [type id]} (problematic-plugin-source url)
|
||||
uid (keyword (str "plugin-illegal-package-error-" (hash url)))]
|
||||
(notification/show!
|
||||
[:div.flex.flex-col.gap-2
|
||||
[:div "Failed to parse the plugin package config."]
|
||||
[:div.text-xs.opacity-70.break-all package-json-path]
|
||||
(when (= type :external)
|
||||
[:div.text-xs.opacity-70
|
||||
"Removing it only detaches the plugin from Logseq and keeps the source folder untouched."])
|
||||
[:div.flex.items-center.gap-2.pt-1
|
||||
(shui/button
|
||||
{:size :sm
|
||||
:on-click (fn []
|
||||
(-> (remove-problematic-plugin! url)
|
||||
(p/then (fn [_]
|
||||
(notification/clear! uid)
|
||||
(notification/show!
|
||||
(if (= type :installed)
|
||||
(str "Removed broken plugin \"" id "\".")
|
||||
"Removed the broken plugin from the plugin list.")
|
||||
:success)))
|
||||
(p/catch (fn [error]
|
||||
(notification/show!
|
||||
(str "Failed to remove the broken plugin.\n" error)
|
||||
:error)))))}
|
||||
(t :plugin/uninstall))]]
|
||||
:error false uid)
|
||||
true)
|
||||
(notification/show! "Illegal Logseq plugin package." :error)))
|
||||
|
||||
(defn setup-global-apis-for-web!
|
||||
[]
|
||||
@@ -71,8 +171,8 @@
|
||||
[theme]
|
||||
(when theme
|
||||
(cond-> theme
|
||||
(util/electron?)
|
||||
(update :url #(some-> % (string/replace-first "assets://" "file://"))))))
|
||||
(util/electron?)
|
||||
(update :url #(some-> % (string/replace-first "assets://" "file://"))))))
|
||||
|
||||
(defn load-plugin-preferences
|
||||
[]
|
||||
@@ -108,7 +208,7 @@
|
||||
(if-let [res (and res (bean/->clj res))]
|
||||
(let [pkgs (:packages res)
|
||||
pkgs (if (util/electron?) pkgs
|
||||
(some->> pkgs (filterv #(or (true? (:web %)) (not (true? (:effect %)))))))]
|
||||
(some->> pkgs (filterv #(or (true? (:web %)) (not (true? (:effect %)))))))]
|
||||
(state/set-state! :plugin/marketplace-pkgs pkgs)
|
||||
(resolve pkgs))
|
||||
(reject nil)))]
|
||||
@@ -131,8 +231,8 @@
|
||||
:plugin/marketplace-stats
|
||||
(into {} (map (fn [[k stat]]
|
||||
[k (assoc stat
|
||||
:total_downloads
|
||||
(reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))])
|
||||
:total_downloads
|
||||
(reduce (fn [a b] (+ a (get b 2))) 0 (:releases stat)))])
|
||||
res)))
|
||||
(resolve nil))
|
||||
(reject nil)))]
|
||||
@@ -293,8 +393,8 @@
|
||||
(defn- normalize-plugin-metadata
|
||||
[metadata]
|
||||
(cond-> metadata
|
||||
(not (string? (:author metadata)))
|
||||
(assoc :author (or (get-in metadata [:author :name]) ""))))
|
||||
(not (string? (:author metadata)))
|
||||
(assoc :author (or (get-in metadata [:author :name]) ""))))
|
||||
|
||||
(defn register-plugin
|
||||
[plugin-metadata]
|
||||
@@ -431,7 +531,7 @@
|
||||
(defn- create-local-renderer-register
|
||||
[type *providers]
|
||||
(fn [pid key {subs' :subs :keys [render] :as opts}]
|
||||
(when-let [key (and key (keyword key))]
|
||||
(when-let [key (some-> key (normalize-user-key-without-ns) (keyword))]
|
||||
(register-plugin-resources pid type
|
||||
(merge opts {:key key :subs subs' :render render}))
|
||||
(swap! *providers conj pid)
|
||||
@@ -442,7 +542,7 @@
|
||||
([type *providers many?]
|
||||
(fn [key]
|
||||
(when (seq @*providers)
|
||||
(if key
|
||||
(if-let [key (some-> key (normalize-user-key-without-ns) (keyword))]
|
||||
(when-let [rs (->> @*providers
|
||||
(map (fn [pid] (state/get-plugin-resource pid type key)))
|
||||
(remove nil?)
|
||||
@@ -495,6 +595,24 @@
|
||||
(create-local-renderer-getter
|
||||
:daemon-renderers *daemon-renderer-providers true))
|
||||
|
||||
(defonce *hosted-renderer-providers (atom #{}))
|
||||
(def register-hosted-renderer
|
||||
;; [pid key payload]
|
||||
(create-local-renderer-register
|
||||
:hosted-renderers *hosted-renderer-providers))
|
||||
(def get-hosted-renderers
|
||||
;; [key]
|
||||
(create-local-renderer-getter
|
||||
:hosted-renderers *hosted-renderer-providers true))
|
||||
|
||||
(defn resolve-hosted-render
|
||||
[pid key type]
|
||||
(some->> (get-hosted-renderers)
|
||||
(medley/find-first #(and (some-> (:pid %) (name) (= pid))
|
||||
(or (some-> (:key %) (name) (= key))
|
||||
(some-> (:key %) (str) (string/includes? (str "." key))))
|
||||
(some->> type (name) (= (:type %)))))))
|
||||
|
||||
(defn select-a-plugin-theme
|
||||
[pid]
|
||||
(when-let [themes (get (group-by :pid (:plugin/installed-themes @state/state)) pid)]
|
||||
@@ -562,7 +680,7 @@
|
||||
(let [repo (:repo item)]
|
||||
(if (nil? repo)
|
||||
;; local
|
||||
(-> (p/let [content (invoke-exported-api "load_plugin_readme" url)
|
||||
(-> (p/let [content (invoke-exported-api :load_plugin_readme url)
|
||||
content (parse-user-md-content content item)]
|
||||
(and (string/blank? (string/trim content)) (throw (js/Error. "blank readme content")))
|
||||
(state/set-state! :plugin/active-readme [content item])
|
||||
@@ -822,8 +940,8 @@
|
||||
:theme theme?
|
||||
:web-pkg (cond-> package
|
||||
|
||||
(not github?)
|
||||
(assoc :installedFromUserWebUrl url))}}))
|
||||
(not github?)
|
||||
(assoc :installedFromUserWebUrl url))}}))
|
||||
url)))
|
||||
|
||||
;; components
|
||||
@@ -851,8 +969,8 @@
|
||||
clear-commands! (fn [pid]
|
||||
;; commands
|
||||
(unregister-plugin-slash-command pid)
|
||||
(invoke-exported-api "unregister_plugin_simple_command" pid)
|
||||
(invoke-exported-api "uninstall_plugin_hook" pid)
|
||||
(invoke-exported-api :unregister_plugin_simple_command pid)
|
||||
(invoke-exported-api :uninstall_plugin_hook pid)
|
||||
(unregister-plugin-ui-items pid)
|
||||
(unregister-plugin-resources pid)
|
||||
(unregister-plugin-search-services pid))
|
||||
@@ -863,6 +981,11 @@
|
||||
(register-plugin
|
||||
(bean/->clj (.parse js/JSON (.stringify js/JSON pl))))))
|
||||
|
||||
(.on "error"
|
||||
(fn [^js e]
|
||||
(when (illegal-plugin-package-error->data e)
|
||||
(show-illegal-plugin-package-notification! e))))
|
||||
|
||||
(.on "beforeload"
|
||||
(fn [^js pl]
|
||||
(let [text (when (util/electron?)
|
||||
@@ -916,7 +1039,10 @@
|
||||
(.on "reset-custom-theme" (fn [^js themes]
|
||||
(let [themes (bean/->clj themes)
|
||||
custom-theme (dissoc themes :mode)
|
||||
mode (:mode themes)]
|
||||
;; Fall back to the user's current theme so that
|
||||
;; installing a non-theme plugin does not flash
|
||||
;; the UI back to light mode (#12434).
|
||||
mode (or (:mode themes) (:ui/theme @state/state))]
|
||||
(state/set-custom-theme! {:light (if (nil? (:light custom-theme)) {:mode "light"} (:light custom-theme))
|
||||
:dark (if (nil? (:dark custom-theme)) {:mode "dark"} (:dark custom-theme))})
|
||||
(state/set-theme-mode! mode))))
|
||||
@@ -978,8 +1104,8 @@
|
||||
(init-plugins!)))
|
||||
|
||||
(comment
|
||||
{:pending (count (:plugin/updates-pending @state/state))
|
||||
:auto-checking? (boolean (:plugin/updates-auto-checking? @state/state))
|
||||
:coming (count (:plugin/updates-coming @state/state))
|
||||
:installing (:plugin/installing @state/state)
|
||||
:downloading? (boolean (:plugin/updates-downloading? @state/state))})
|
||||
{:pending (count (:plugin/updates-pending @state/state))
|
||||
:auto-checking? (boolean (:plugin/updates-auto-checking? @state/state))
|
||||
:coming (count (:plugin/updates-coming @state/state))
|
||||
:installing (:plugin/installing @state/state)
|
||||
:downloading? (boolean (:plugin/updates-downloading? @state/state))})
|
||||
|
||||
@@ -7,16 +7,14 @@
|
||||
(defn remove-block-property!
|
||||
[block-id property-id-or-key]
|
||||
(assert (some? property-id-or-key) "remove-block-property! remove-block-property! is nil")
|
||||
(let [eid (if (uuid? block-id) [:block/uuid block-id] block-id)]
|
||||
(db-property-handler/remove-block-property! eid property-id-or-key)))
|
||||
(db-property-handler/remove-block-property! block-id property-id-or-key))
|
||||
|
||||
(defn set-block-property!
|
||||
[block-id key v]
|
||||
(assert (some? key) "set-block-property! key is nil")
|
||||
(let [eid (if (uuid? block-id) [:block/uuid block-id] block-id)]
|
||||
(if (or (nil? v) (and (coll? v) (empty? v)))
|
||||
(db-property-handler/remove-block-property! eid key)
|
||||
(db-property-handler/set-block-property! eid key v))))
|
||||
(if (or (nil? v) (and (coll? v) (empty? v)))
|
||||
(db-property-handler/remove-block-property! block-id key)
|
||||
(db-property-handler/set-block-property! block-id key v)))
|
||||
|
||||
(defn batch-remove-block-property!
|
||||
[block-ids key]
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn search
|
||||
"The aggretation of search results"
|
||||
"The aggregation of search results"
|
||||
([q]
|
||||
(search (state/get-current-repo) q))
|
||||
([repo q]
|
||||
|
||||
@@ -355,7 +355,8 @@
|
||||
|
||||
(defn rtc-group?
|
||||
[]
|
||||
(boolean (seq (set/intersection (state/user-groups) #{"team" "rtc_2025_07_10"}))))
|
||||
(boolean (or (some? (config/get-custom-sync-server-url))
|
||||
(seq (set/intersection (state/user-groups) #{"team" "rtc_2025_07_10"})))))
|
||||
|
||||
(defn alpha-user?
|
||||
[]
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
(ns frontend.modules.outliner.op
|
||||
"Build outliner ops"
|
||||
(:require [datascript.impl.entity :as de]
|
||||
[frontend.handler.user :as user-handler]))
|
||||
[frontend.handler.user :as user-handler]
|
||||
[frontend.db.utils :as db-utils]))
|
||||
|
||||
(defn- current-user-delete-opts
|
||||
[opts]
|
||||
@@ -21,116 +22,141 @@
|
||||
(conj! *outliner-ops* result)
|
||||
result)
|
||||
|
||||
(defn- ->block-id
|
||||
[block-or-id]
|
||||
(cond
|
||||
(de/entity? block-or-id)
|
||||
(:block/uuid block-or-id)
|
||||
|
||||
(map? block-or-id)
|
||||
(:block/uuid block-or-id)
|
||||
|
||||
(number? block-or-id)
|
||||
(:block/uuid (db-utils/entity block-or-id))
|
||||
|
||||
:else
|
||||
block-or-id))
|
||||
|
||||
(defn- ->property-id
|
||||
[property-id]
|
||||
(cond
|
||||
(number? property-id)
|
||||
(:db/ident (db-utils/entity property-id))
|
||||
|
||||
:else
|
||||
property-id))
|
||||
|
||||
(defn save-block!
|
||||
[block & {:as opts}]
|
||||
(op-transact!
|
||||
(when-let [block' (if (de/entity? block)
|
||||
(assoc (.-kv ^js block) :db/id (:db/id block))
|
||||
(dissoc (.-kv ^js block) :db/id)
|
||||
block)]
|
||||
[:save-block [block' opts]])))
|
||||
|
||||
(defn insert-blocks!
|
||||
[blocks target-block opts]
|
||||
(op-transact!
|
||||
(let [id (:db/id target-block)]
|
||||
(let [id (->block-id target-block)]
|
||||
[:insert-blocks [blocks id opts]])))
|
||||
|
||||
(defn apply-template!
|
||||
[template-id target-block opts]
|
||||
(op-transact!
|
||||
(let [id (:db/id target-block)]
|
||||
[:apply-template [template-id id opts]])))
|
||||
(let [template-id' (->block-id template-id)
|
||||
id (->block-id target-block)]
|
||||
[:apply-template [template-id' id opts]])))
|
||||
|
||||
(defn delete-blocks!
|
||||
[blocks opts]
|
||||
(op-transact!
|
||||
(let [ids (map :db/id blocks)]
|
||||
(let [ids (map ->block-id blocks)]
|
||||
(when (seq ids)
|
||||
[:delete-blocks [ids (current-user-delete-opts opts)]]))))
|
||||
|
||||
(defn move-blocks!
|
||||
[blocks target-block opts]
|
||||
(op-transact!
|
||||
(let [ids (map :db/id blocks)
|
||||
target-id (:db/id target-block)]
|
||||
(let [ids (map ->block-id blocks)
|
||||
target-id (->block-id target-block)]
|
||||
[:move-blocks [ids target-id opts]])))
|
||||
|
||||
(defn move-blocks-up-down!
|
||||
[blocks up?]
|
||||
(op-transact!
|
||||
(let [ids (map :db/id blocks)]
|
||||
(let [ids (map ->block-id blocks)]
|
||||
[:move-blocks-up-down [ids up?]])))
|
||||
|
||||
(defn indent-outdent-blocks!
|
||||
[blocks indent? & {:as opts}]
|
||||
(op-transact!
|
||||
(let [ids (map :db/id blocks)]
|
||||
(let [ids (map ->block-id blocks)]
|
||||
[:indent-outdent-blocks [ids indent? opts]])))
|
||||
|
||||
(defn upsert-property!
|
||||
[property-id schema property-opts]
|
||||
(op-transact!
|
||||
[:upsert-property [property-id schema property-opts]]))
|
||||
[:upsert-property [(->property-id property-id) schema property-opts]]))
|
||||
|
||||
(defn set-block-property!
|
||||
[block-eid property-id value]
|
||||
(op-transact!
|
||||
[:set-block-property [block-eid property-id value]]))
|
||||
[:set-block-property [(->block-id block-eid) (->property-id property-id) value]]))
|
||||
|
||||
(defn remove-block-property!
|
||||
[block-eid property-id]
|
||||
(op-transact!
|
||||
[:remove-block-property [block-eid property-id]]))
|
||||
[:remove-block-property [(->block-id block-eid) (->property-id property-id)]]))
|
||||
|
||||
(defn delete-property-value!
|
||||
[block-eid property-id property-value]
|
||||
(op-transact!
|
||||
[:delete-property-value [block-eid property-id property-value]]))
|
||||
[:delete-property-value [(->block-id block-eid) (->property-id property-id) property-value]]))
|
||||
|
||||
(defn batch-delete-property-value!
|
||||
[block-eids property-id property-value]
|
||||
(op-transact!
|
||||
[:batch-delete-property-value [block-eids property-id property-value]]))
|
||||
[:batch-delete-property-value [(mapv ->block-id block-eids) (->property-id property-id) property-value]]))
|
||||
|
||||
(defn create-property-text-block!
|
||||
[block-id property-id value opts]
|
||||
(op-transact!
|
||||
[:create-property-text-block [block-id property-id value opts]]))
|
||||
[:create-property-text-block [(->block-id block-id) (->property-id property-id) value opts]]))
|
||||
|
||||
(defn batch-set-property!
|
||||
[block-ids property-id value opts]
|
||||
(op-transact!
|
||||
[:batch-set-property [block-ids property-id value opts]]))
|
||||
[:batch-set-property [(mapv ->block-id block-ids) (->property-id property-id) value opts]]))
|
||||
|
||||
(defn batch-remove-property!
|
||||
[block-ids property-id]
|
||||
(op-transact!
|
||||
[:batch-remove-property [block-ids property-id]]))
|
||||
[:batch-remove-property [(mapv ->block-id block-ids) (->property-id property-id)]]))
|
||||
|
||||
(defn class-add-property!
|
||||
[class-id property-id]
|
||||
(op-transact!
|
||||
[:class-add-property [class-id property-id]]))
|
||||
[:class-add-property [(->block-id class-id) (->property-id property-id)]]))
|
||||
|
||||
(defn class-remove-property!
|
||||
[class-id property-id]
|
||||
(op-transact!
|
||||
[:class-remove-property [class-id property-id]]))
|
||||
[:class-remove-property [(->block-id class-id) (->property-id property-id)]]))
|
||||
|
||||
(defn upsert-closed-value!
|
||||
[property-id closed-value-config]
|
||||
(op-transact!
|
||||
[:upsert-closed-value [property-id closed-value-config]]))
|
||||
[:upsert-closed-value [(->property-id property-id) closed-value-config]]))
|
||||
|
||||
(defn delete-closed-value!
|
||||
[property-id value-block-id]
|
||||
(op-transact!
|
||||
[:delete-closed-value [property-id value-block-id]]))
|
||||
[:delete-closed-value [(->property-id property-id) (->block-id value-block-id)]]))
|
||||
|
||||
(defn add-existing-values-to-closed-values!
|
||||
[property-id values]
|
||||
(op-transact!
|
||||
[:add-existing-values-to-closed-values [property-id values]]))
|
||||
[:add-existing-values-to-closed-values [(->property-id property-id) values]]))
|
||||
|
||||
(defn toggle-reaction!
|
||||
[target-uuid emoji-id user-uuid]
|
||||
@@ -163,3 +189,8 @@
|
||||
([page-uuid opts]
|
||||
(op-transact!
|
||||
[:delete-page [page-uuid (current-user-delete-opts opts)]])))
|
||||
|
||||
(defn recycle-delete-permanently!
|
||||
[root-uuid]
|
||||
(op-transact!
|
||||
[:recycle-delete-permanently [root-uuid]]))
|
||||
|
||||
@@ -511,6 +511,9 @@
|
||||
:dev/recompute-checksum {:binding []
|
||||
:inactive (not (state/developer-mode?))
|
||||
:fn :frontend.handler.common.developer/recompute-checksum-diagnostics}
|
||||
:dev/export-client-ops-sqlite {:binding []
|
||||
:inactive (not (state/developer-mode?))
|
||||
:fn :frontend.handler.common.developer/export-client-ops-sqlite}
|
||||
:dev/rtc-stop {:binding []
|
||||
:inactive (not (state/developer-mode?))
|
||||
:fn :frontend.handler.common.developer/rtc-stop}
|
||||
@@ -714,6 +717,7 @@
|
||||
:dev/replace-graph-with-db-file
|
||||
:dev/validate-db
|
||||
:dev/recompute-checksum
|
||||
:dev/export-client-ops-sqlite
|
||||
:dev/gc-graph
|
||||
:dev/rtc-stop
|
||||
:dev/rtc-start
|
||||
@@ -878,6 +882,7 @@
|
||||
:dev/replace-graph-with-db-file
|
||||
:dev/validate-db
|
||||
:dev/recompute-checksum
|
||||
:dev/export-client-ops-sqlite
|
||||
:dev/gc-graph
|
||||
:dev/rtc-stop
|
||||
:dev/rtc-start
|
||||
|
||||
@@ -21,6 +21,38 @@
|
||||
[repo diff]
|
||||
(state/input-idle? repo :diff diff))
|
||||
|
||||
(def-thread-api :thread-api/search-index-build-progress
|
||||
[repo {:keys [status progress processed total]}]
|
||||
(let [prev-state (get @state/state :search/index-build)
|
||||
current-repo (state/get-current-repo)
|
||||
visible-repo? (or (= repo current-repo)
|
||||
(= repo (:repo prev-state)))]
|
||||
(when visible-repo?
|
||||
(case status
|
||||
:idle
|
||||
(state/set-state! :search/index-build
|
||||
(assoc (or prev-state {})
|
||||
:running? false
|
||||
:repo repo))
|
||||
|
||||
:running
|
||||
(state/set-state! :search/index-build
|
||||
{:running? true
|
||||
:repo repo
|
||||
:progress (or progress 0)
|
||||
:processed (or processed 0)
|
||||
:total (or total 0)})
|
||||
|
||||
:completed
|
||||
(state/set-state! :search/index-build
|
||||
{:running? false
|
||||
:repo repo
|
||||
:progress (or progress 0)
|
||||
:processed (or processed 0)
|
||||
:total (or total 0)})
|
||||
nil))
|
||||
nil))
|
||||
|
||||
(defn- ask-persist-permission!
|
||||
[]
|
||||
(p/let [persistent? (.persist js/navigator.storage)]
|
||||
@@ -132,8 +164,8 @@
|
||||
(-> (p/let [_ (state/<invoke-db-worker :thread-api/init)
|
||||
_ (state/<invoke-db-worker :thread-api/set-db-sync-config
|
||||
{:enabled? true
|
||||
:ws-url config/db-sync-ws-url
|
||||
:http-base config/db-sync-http-base})
|
||||
:ws-url (config/db-sync-ws-url)
|
||||
:http-base (config/db-sync-http-base)})
|
||||
_ (state/pub-event! [:rtc/sync-app-state])
|
||||
_ (log/info "init worker spent" (str (- (util/time-ms) t1) "ms"))
|
||||
_ (sync-ui-state!)
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
(s/def :copy/export-block-text-indent-style string?)
|
||||
(s/def :copy/export-block-text-remove-options set?)
|
||||
(s/def :copy/export-block-text-other-options map?)
|
||||
(s/def ::sync-server-url string?)
|
||||
;; Dynamic keys which aren't as easily validated:
|
||||
;; :ls-pdf-last-page-*
|
||||
;; :ls-js-allowed-*
|
||||
@@ -66,4 +67,5 @@
|
||||
:ui/shortcut-tooltip?
|
||||
:copy/export-block-text-indent-style
|
||||
:copy/export-block-text-remove-options
|
||||
:copy/export-block-text-other-options]))
|
||||
:copy/export-block-text-other-options
|
||||
::sync-server-url]))
|
||||
|
||||
@@ -89,6 +89,11 @@
|
||||
:search/result nil
|
||||
:search/graph-filters []
|
||||
:search/engines {}
|
||||
:search/index-build {:running? false
|
||||
:repo nil
|
||||
:progress 0
|
||||
:processed 0
|
||||
:total 0}
|
||||
|
||||
;; modals
|
||||
:modal/dropdowns {}
|
||||
|
||||
@@ -298,8 +298,6 @@
|
||||
(notification/clear! uid))
|
||||
:icon "x"})]]]]]])))
|
||||
|
||||
(declare button)
|
||||
|
||||
(rum/defc notification-clear-all
|
||||
[]
|
||||
[:div.ui__notifications-content
|
||||
|
||||
@@ -1314,16 +1314,20 @@
|
||||
(.catch (fn [err]
|
||||
(js/console.error "Web clipboard failed" err)))))))))
|
||||
|
||||
#?(:cljs
|
||||
(defn copy-image-blob-to-clipboard
|
||||
[blob]
|
||||
(if (= (.-type blob) "image/png")
|
||||
(write-blob-to-clipboard blob)
|
||||
(image-blob->png blob write-blob-to-clipboard))))
|
||||
|
||||
#?(:cljs
|
||||
(defn copy-image-to-clipboard
|
||||
[src]
|
||||
(-> (js/fetch src)
|
||||
(.then (fn [data]
|
||||
(-> (.blob data)
|
||||
(.then (fn [blob]
|
||||
(if (= (.-type blob) "image/png")
|
||||
(write-blob-to-clipboard blob)
|
||||
(image-blob->png blob write-blob-to-clipboard))))
|
||||
(.then copy-image-blob-to-clipboard)
|
||||
(.catch js/console.error)))))))
|
||||
|
||||
(defn memoize-last
|
||||
|
||||
@@ -124,6 +124,7 @@
|
||||
nil)))
|
||||
|
||||
(def repo-path "/db.sqlite")
|
||||
(def client-ops-repo-path (str "client-ops" repo-path))
|
||||
|
||||
(defn- resolve-db-path
|
||||
[repo pool path]
|
||||
@@ -141,6 +142,44 @@
|
||||
(let [storage (platform/storage (platform/current))]
|
||||
((:export-file storage) pool path))))))
|
||||
|
||||
(defn- ->uint8array
|
||||
[data]
|
||||
(cond
|
||||
(instance? js/Uint8Array data)
|
||||
data
|
||||
|
||||
(js/ArrayBuffer.isView data)
|
||||
(js/Uint8Array. (.-buffer data) (.-byteOffset data) (.-byteLength data))
|
||||
|
||||
(instance? js/ArrayBuffer data)
|
||||
(js/Uint8Array. data)
|
||||
|
||||
(array? data)
|
||||
(js/Uint8Array. data)
|
||||
|
||||
:else
|
||||
data))
|
||||
|
||||
(defn- <export-db-file-with-paths
|
||||
[repo path-candidates]
|
||||
(let [paths (->> path-candidates
|
||||
(filter string?)
|
||||
(remove string/blank?)
|
||||
distinct
|
||||
vec)]
|
||||
(letfn [(try-export [remaining-paths]
|
||||
(if-let [path (first remaining-paths)]
|
||||
(-> (<export-db-file repo path)
|
||||
(p/then (fn [result]
|
||||
(let [payload (->uint8array result)]
|
||||
(if (instance? js/Uint8Array payload)
|
||||
payload
|
||||
(try-export (subvec remaining-paths 1))))))
|
||||
(p/catch (fn [_]
|
||||
(try-export (subvec remaining-paths 1)))))
|
||||
(p/resolved nil)))]
|
||||
(try-export paths))))
|
||||
|
||||
(defn- <import-db
|
||||
[^js pool data]
|
||||
(let [storage (platform/storage (platform/current))]
|
||||
@@ -514,6 +553,18 @@
|
||||
[repo]
|
||||
(db-sync/upload-graph! repo))
|
||||
|
||||
(def-thread-api :thread-api/db-sync-stop-upload
|
||||
[repo]
|
||||
(db-sync/stop-upload! repo))
|
||||
|
||||
(def-thread-api :thread-api/db-sync-resume-upload
|
||||
[repo]
|
||||
(db-sync/resume-upload! repo))
|
||||
|
||||
(def-thread-api :thread-api/db-sync-upload-stopped?
|
||||
[repo]
|
||||
(db-sync/upload-stopped? repo))
|
||||
|
||||
(def-thread-api :thread-api/db-sync-download-graph
|
||||
[repo]
|
||||
(sync-download/download-graph! repo))
|
||||
@@ -778,6 +829,26 @@
|
||||
(p/let [data (<export-db-file repo)]
|
||||
(platform/transfer (platform/current) data #js [(.-buffer data)])))
|
||||
|
||||
(def-thread-api :thread-api/export-client-ops-db
|
||||
[repo]
|
||||
(when-let [^js db (worker-state/get-sqlite-conn repo :client-ops)]
|
||||
(.exec db "PRAGMA wal_checkpoint(2)"))
|
||||
(let [^js client-ops-db (worker-state/get-sqlite-conn repo :client-ops)
|
||||
^js pool (get-storage-pool repo)
|
||||
db-filename (some-> client-ops-db .-filename)
|
||||
resolved-client-ops-path (when pool
|
||||
(resolve-db-path repo pool (str "client-ops-" repo-path)))
|
||||
export-paths [db-filename
|
||||
resolved-client-ops-path
|
||||
client-ops-repo-path
|
||||
(str "/" client-ops-repo-path)
|
||||
(str "client-ops" repo-path)
|
||||
(str "/client-ops" repo-path)
|
||||
(str "client-ops-" repo-path)]]
|
||||
(p/let [payload (<export-db-file-with-paths repo export-paths)]
|
||||
(when (instance? js/Uint8Array payload)
|
||||
(platform/transfer (platform/current) payload #js [(.-buffer payload)])))))
|
||||
|
||||
(def-thread-api :thread-api/export-db-base64
|
||||
[repo]
|
||||
(when-let [^js db (worker-state/get-sqlite-conn repo :db)]
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
[frontend.worker.search :as search]
|
||||
[frontend.worker.shared-service :as shared-service]
|
||||
[frontend.worker.state :as worker-state]
|
||||
[frontend.worker-common.util :as worker-util]
|
||||
[frontend.worker.sync :as db-sync]
|
||||
[promesa.core :as p]))
|
||||
|
||||
@@ -70,14 +71,16 @@
|
||||
(d/unlisten! conn ::listen-db-changes!)
|
||||
(d/listen! conn ::listen-db-changes!
|
||||
(fn listen-db-changes!-inner
|
||||
[{:keys [tx-data] :as tx-report}]
|
||||
(when-not (:batch-tx? @conn)
|
||||
(when (seq tx-data)
|
||||
[{:keys [tx-data tx-meta] :as tx-report}]
|
||||
(when (seq tx-data)
|
||||
(when (and worker-util/dev-or-test?
|
||||
(not (:batch-final-tx-report? tx-meta)))
|
||||
(db-sync/update-local-sync-checksum! repo tx-report))
|
||||
(when-not (:batch-tx? @conn)
|
||||
(let [tx-report' (if sync-db-to-main-thread?
|
||||
(sync-db-to-main-thread repo conn tx-report)
|
||||
tx-report)
|
||||
opt {:repo repo}]
|
||||
(when tx-report'
|
||||
(db-sync/update-local-sync-checksum! repo tx-report')
|
||||
(doseq [[k handler-fn] handlers]
|
||||
(handler-fn k opt tx-report'))))))))))
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
[logseq.graph-parser.exporter :as gp-exporter]
|
||||
[logseq.outliner.core :as outliner-core]
|
||||
[logseq.outliner.datascript-report :as ds-report]
|
||||
[logseq.outliner.template :as outliner-template]
|
||||
[logseq.outliner.pipeline :as outliner-pipeline]))
|
||||
|
||||
(def ^:private rtc-tx-or-download-graph?
|
||||
@@ -67,43 +68,51 @@
|
||||
journal-page (some (fn [d] (when (and (= :block/journal-day (:a d)) (:added d))
|
||||
(d/entity db (:e d))))
|
||||
(:tx-data tx-report))
|
||||
journal-template? (some (fn [d] (and (:added d) (= (:a d) :block/tags) (= (:v d) journal-id))) (:tx-data tx-report))
|
||||
tx-data (some->> (:tx-data tx-report)
|
||||
(filter (fn [d] (and (= (:a d) :block/tags) (:added d))))
|
||||
(group-by :e)
|
||||
(mapcat (fn [[e datoms]]
|
||||
(let [object (d/entity db e)
|
||||
template-blocks (->> (mapcat (fn [id]
|
||||
(let [tag (d/entity db id)
|
||||
parents (ldb/get-class-extends tag)
|
||||
templates (mapcat :logseq.property/_template-applied-to (conj parents tag))]
|
||||
(cond->> templates
|
||||
journal-page
|
||||
(map (fn [t] (assoc t :journal journal-page))))))
|
||||
(set (map :v datoms)))
|
||||
distinct
|
||||
(sort-by :block/created-at)
|
||||
(mapcat (fn [template]
|
||||
(let [template-blocks (rest (ldb/get-block-and-children db (:block/uuid template)
|
||||
{:include-property-block? true}))
|
||||
blocks (->>
|
||||
(cons (assoc (first template-blocks) :logseq.property/used-template (:db/id template))
|
||||
(rest template-blocks))
|
||||
(map (fn [e]
|
||||
(cond->
|
||||
(assoc (into {} e) :db/id (:db/id e))
|
||||
(:journal template)
|
||||
(assoc :block/uuid
|
||||
(common-uuid/gen-journal-template-block (:block/uuid (:journal template))
|
||||
(:block/uuid e)))))))]
|
||||
blocks))))]
|
||||
(when (seq template-blocks)
|
||||
(let [result (outliner-core/insert-blocks
|
||||
db template-blocks object
|
||||
{:sibling? false
|
||||
:keep-uuid? journal-template?
|
||||
:outliner-op :insert-template-blocks})]
|
||||
(:tx-data result)))))))]
|
||||
journal-template? (some (fn [d] (and (:added d)
|
||||
(= (:a d) :block/tags)
|
||||
(= (:v d) journal-id)))
|
||||
(:tx-data tx-report))
|
||||
tag->templates (fn [id]
|
||||
(let [tag (d/entity db id)
|
||||
parents (ldb/get-class-extends tag)
|
||||
templates (mapcat :logseq.property/_template-applied-to (conj parents tag))]
|
||||
(cond->> templates
|
||||
journal-page
|
||||
(map (fn [t] (assoc t :journal journal-page))))))
|
||||
template->blocks (fn [object template]
|
||||
(let [template-children (rest (ldb/get-block-and-children db (:block/uuid template)
|
||||
{:include-property-block? true}))
|
||||
blocks (->> (cons (assoc (first template-children)
|
||||
:logseq.property/used-template (:db/id template))
|
||||
(rest template-children))
|
||||
(map (fn [block]
|
||||
(cond->
|
||||
(assoc (into {} block) :db/id (:db/id block))
|
||||
(:journal template)
|
||||
(assoc :block/uuid
|
||||
(common-uuid/gen-journal-template-block
|
||||
(:block/uuid (:journal template))
|
||||
(:block/uuid block)))))))]
|
||||
(outliner-template/resolve-dynamic-template-blocks db object blocks)))
|
||||
tag-additions (->> (:tx-data tx-report)
|
||||
(filter (fn [d] (and (= (:a d) :block/tags) (:added d))))
|
||||
(group-by :e))
|
||||
tx-data (mapcat
|
||||
(fn [[e datoms]]
|
||||
(let [object (d/entity db e)
|
||||
templates (->> (set (map :v datoms))
|
||||
(mapcat tag->templates)
|
||||
distinct
|
||||
(sort-by :block/created-at))
|
||||
blocks-to-insert (mapcat (partial template->blocks object) templates)]
|
||||
(when (seq blocks-to-insert)
|
||||
(let [result (outliner-core/insert-blocks
|
||||
db blocks-to-insert object
|
||||
{:sibling? false
|
||||
:keep-uuid? journal-template?
|
||||
:outliner-op :insert-template-blocks})]
|
||||
(:tx-data result)))))
|
||||
tag-additions)]
|
||||
tx-data))
|
||||
|
||||
(defn- fix-page-tags
|
||||
@@ -125,9 +134,12 @@
|
||||
(not (ldb/inline-tag? (:block/raw-title entity) tag))
|
||||
(not (:db/ident entity)))
|
||||
(let [eid (:db/id entity)]
|
||||
[[:db/add eid :db/ident (db-class/create-user-class-ident-from-name db-after (:block/title entity))]
|
||||
[:db/add eid :logseq.property.class/extends :logseq.class/Root]
|
||||
[:db/retract eid :block/tags :logseq.class/Page]])
|
||||
(if (:block/page entity)
|
||||
;; Built-in #Tag should never turn a page child block into a class.
|
||||
[[:db/retract eid :block/tags :logseq.class/Tag]]
|
||||
[[:db/add eid :db/ident (db-class/create-user-class-ident-from-name db-after (:block/title entity))]
|
||||
[:db/add eid :logseq.property.class/extends :logseq.class/Root]
|
||||
[:db/retract eid :block/tags :logseq.class/Page]]))
|
||||
|
||||
;; remove #Page from tags/journals etc.
|
||||
(= (:db/id page-tag) (:v datom))
|
||||
@@ -439,7 +451,7 @@
|
||||
display-blocks-tx-data (add-missing-properties-to-typed-display-blocks db-after tx-data tx-meta)
|
||||
ensure-query-tx-data (ensure-query-property-on-tag-additions tx-report)
|
||||
commands-tx (when-not (or (:undo? tx-meta)
|
||||
(contains? #{:rebase} (:outliner-op tx-meta))
|
||||
(= :rebase (:outliner-op tx-meta))
|
||||
(rtc-tx-or-download-graph? tx-meta))
|
||||
(commands/run-commands tx-report))
|
||||
insert-templates-tx (when-not (rtc-tx-or-download-graph? tx-meta)
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
(s/def ::objects (s/tuple #(= ::objects %) int?))
|
||||
;; get block reactions
|
||||
(s/def ::block-reactions (s/tuple #(= ::block-reactions %) int?))
|
||||
;; recycle roots list
|
||||
(s/def ::recycle-roots (s/tuple #(= ::recycle-roots %)))
|
||||
;; custom react-query
|
||||
(s/def ::custom any?)
|
||||
|
||||
@@ -27,13 +29,21 @@
|
||||
:refs ::refs
|
||||
:objects ::objects
|
||||
:block-reactions ::block-reactions
|
||||
:recycle-roots ::recycle-roots
|
||||
:custom ::custom))
|
||||
|
||||
(s/def ::affected-keys (s/coll-of ::react-query-keys))
|
||||
|
||||
(defn- journal-page?
|
||||
[db eid journal-tag-id]
|
||||
(when (and db eid journal-tag-id)
|
||||
(some (fn [tag]
|
||||
(= journal-tag-id (:db/id tag)))
|
||||
(:block/tags (d/entity db eid)))))
|
||||
|
||||
(defn get-affected-queries-keys
|
||||
"Get affected queries through transaction datoms."
|
||||
[{:keys [tx-data db-after]}]
|
||||
[{:keys [tx-data db-before db-after]}]
|
||||
{:post [(s/valid? ::affected-keys %)]}
|
||||
(let [blocks (->> (filter (fn [datom] (contains? #{:block/parent :block/page} (:a datom))) tx-data)
|
||||
(map :v)
|
||||
@@ -47,16 +57,26 @@
|
||||
tags (->> (filter (fn [datom] (= :block/tags (:a datom))) tx-data)
|
||||
(map :v)
|
||||
(distinct))
|
||||
journals? (some (fn [datom]
|
||||
(and
|
||||
(= :block/tags (:a datom))
|
||||
(= (:db/id (d/entity db-after :logseq.class/Journal))
|
||||
(:v datom))))
|
||||
tx-data)
|
||||
journal-tag-id (:db/id (d/entity db-after :logseq.class/Journal))
|
||||
touched-eids (->> tx-data (map :e) distinct)
|
||||
journals? (or (some (fn [datom]
|
||||
(and (= :block/tags (:a datom))
|
||||
(= journal-tag-id (:v datom))))
|
||||
tx-data)
|
||||
(some (fn [datom]
|
||||
(= :block/journal-day (:a datom)))
|
||||
tx-data)
|
||||
(some (fn [eid]
|
||||
(or (journal-page? db-before eid journal-tag-id)
|
||||
(journal-page? db-after eid journal-tag-id)))
|
||||
touched-eids))
|
||||
reaction-targets (->> (filter (fn [datom]
|
||||
(= :logseq.property.reaction/target (:a datom))) tx-data)
|
||||
(map :v)
|
||||
(distinct))
|
||||
recycle-roots? (some (fn [datom]
|
||||
(= :logseq.property/deleted-at (:a datom)))
|
||||
tx-data)
|
||||
other-blocks (->> (filter (fn [datom] (= "block" (namespace (:a datom)))) tx-data)
|
||||
(map :e))
|
||||
blocks (-> (concat blocks other-blocks) distinct)
|
||||
@@ -100,6 +120,9 @@
|
||||
(when tag [::objects tag]))
|
||||
tags)
|
||||
|
||||
(when recycle-roots?
|
||||
[[::recycle-roots]])
|
||||
|
||||
(when journals?
|
||||
[[::journals]]))]
|
||||
(->>
|
||||
|
||||
@@ -102,22 +102,44 @@ DROP TRIGGER IF EXISTS blocks_au;
|
||||
(str "(" (->> (map (fn [id] (str "'" id "'")) ids)
|
||||
(string/join ", ")) ")"))
|
||||
|
||||
(def ^:private upsert-blocks-batch-size 2000)
|
||||
|
||||
(def ^:private upsert-blocks-sql
|
||||
(memoize
|
||||
(fn [row-count]
|
||||
(str "INSERT INTO blocks (id, title, page) VALUES "
|
||||
(string/join ", " (repeat row-count "(?, ?, ?)"))
|
||||
" ON CONFLICT (id) DO UPDATE SET (title, page) = (excluded.title, excluded.page)"))))
|
||||
|
||||
(defn- valid-upsert-block?
|
||||
[item]
|
||||
(and (common-util/uuid-string? (.-id item))
|
||||
(common-util/uuid-string? (.-page item))))
|
||||
|
||||
(defn- throw-upsert-blocks-error!
|
||||
[item]
|
||||
(js/console.error "Upsert blocks wrong data: ")
|
||||
(js/console.dir item)
|
||||
(throw (ex-info "Search upsert-blocks wrong data: "
|
||||
(bean/->clj item))))
|
||||
|
||||
(defn- upsert-bind-params
|
||||
[batch]
|
||||
(into-array
|
||||
(mapcat (fn [item]
|
||||
[(.-id item) (.-title item) (.-page item)])
|
||||
batch)))
|
||||
|
||||
(defn upsert-blocks!
|
||||
[^Object db blocks]
|
||||
(assert db ::upsert-blocks!)
|
||||
(.transaction db (fn [tx]
|
||||
(doseq [item blocks]
|
||||
(if (and (common-util/uuid-string? (.-id item))
|
||||
(common-util/uuid-string? (.-page item)))
|
||||
(.exec tx #js {:sql "INSERT INTO blocks (id, title, page) VALUES ($id, $title, $page) ON CONFLICT (id) DO UPDATE SET (title, page) = ($title, $page)"
|
||||
:bind #js {:$id (.-id item)
|
||||
:$title (.-title item)
|
||||
:$page (.-page item)}})
|
||||
(do
|
||||
(js/console.error "Upsert blocks wrong data: ")
|
||||
(js/console.dir item)
|
||||
(throw (ex-info "Search upsert-blocks wrong data: "
|
||||
(bean/->clj item)))))))))
|
||||
(doseq [batch (partition-all upsert-blocks-batch-size blocks)]
|
||||
(doseq [item blocks]
|
||||
(when-not (valid-upsert-block? item)
|
||||
(throw-upsert-blocks-error! item)))
|
||||
(.exec tx #js {:sql (upsert-blocks-sql (count batch))
|
||||
:bind (upsert-bind-params batch)})))))
|
||||
|
||||
(defn delete-blocks!
|
||||
[db ids]
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[frontend.worker.sync.transport :as sync-transport]
|
||||
[frontend.worker.sync.upload :as sync-upload]
|
||||
[frontend.worker.sync.util :as sync-util]
|
||||
[frontend.worker-common.util :as worker-util]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.common.util :as common-util]
|
||||
[logseq.db-sync.checksum :as sync-checksum]
|
||||
@@ -50,10 +51,22 @@
|
||||
|
||||
(defn update-local-sync-checksum!
|
||||
[repo tx-report]
|
||||
(when (worker-state/get-client-ops-conn repo)
|
||||
(client-op/update-local-checksum
|
||||
repo
|
||||
(sync-checksum/update-checksum (client-op/get-local-checksum repo) tx-report))))
|
||||
(when (and worker-util/dev-or-test?
|
||||
(worker-state/get-client-ops-conn repo))
|
||||
(let [current-checksum (client-op/get-local-checksum repo)
|
||||
new-checksum (sync-checksum/update-checksum current-checksum tx-report)]
|
||||
;; (let [full-checksum (sync-checksum/recompute-checksum (:db-after tx-report))]
|
||||
;; (when (not= new-checksum full-checksum)
|
||||
;; (prn :debug
|
||||
;; "checksum-doesn't match"
|
||||
;; {:current-checksum current-checksum
|
||||
;; :new-checksum new-checksum
|
||||
;; :full-checksum full-checksum
|
||||
;; :db-before (ldb/write-transit-str (:db-before tx-report))
|
||||
;; :db-after (ldb/write-transit-str (:db-after tx-report))
|
||||
;; :tx-data (ldb/write-transit-str (:tx-data tx-report))
|
||||
;; :tx-meta (ldb/write-transit-str (:tx-meta tx-report))})))
|
||||
(client-op/update-local-checksum repo new-checksum))))
|
||||
|
||||
(defn- broadcast-rtc-state!
|
||||
[client]
|
||||
@@ -129,6 +142,21 @@
|
||||
[ws message]
|
||||
(sync-transport/send! sync-transport/coerce-ws-client-message ws message))
|
||||
|
||||
(defn- enqueue-receive-message!
|
||||
[client task]
|
||||
(if-let [queue (:receive-queue client)]
|
||||
(swap! queue
|
||||
(fn [prev]
|
||||
(-> (or prev (p/resolved nil))
|
||||
;; Keep queue alive even if one message handler fails.
|
||||
(p/catch (fn [_] nil))
|
||||
(p/then (fn [_] (task)))
|
||||
(p/catch (fn [error]
|
||||
(log/error :db-sync/ws-handle-message-failed
|
||||
{:repo (:repo client)
|
||||
:error error}))))))
|
||||
(task)))
|
||||
|
||||
(defn update-presence!
|
||||
[editing-block-uuid]
|
||||
(when-let [client @worker-state/*db-sync-client]
|
||||
@@ -147,7 +175,9 @@
|
||||
[repo]
|
||||
{:repo repo
|
||||
:send-queue (atom (p/resolved nil))
|
||||
:receive-queue (atom (p/resolved nil))
|
||||
:asset-queue (atom (p/resolved nil))
|
||||
:pending-pull-since (atom nil)
|
||||
:inflight (atom [])
|
||||
:last-sync-error (atom nil)
|
||||
:reconnect (atom {:attempt 0 :timer nil})
|
||||
@@ -186,7 +216,9 @@
|
||||
(set! (.-onmessage ws)
|
||||
(fn [event]
|
||||
(touch-last-ws-message! client)
|
||||
(sync-handle-message/handle-message! repo client (.-data event))))
|
||||
(enqueue-receive-message! client
|
||||
(fn []
|
||||
(sync-handle-message/handle-message! repo client (.-data event))))))
|
||||
(set! (.-onerror ws) (fn [error] (log/error :db-sync/ws-error error)))
|
||||
(set! (.-onclose ws)
|
||||
(fn [_]
|
||||
@@ -360,3 +392,15 @@
|
||||
(sync-upload/upload-graph! repo))
|
||||
|
||||
(def list-remote-graphs! sync-upload/list-remote-graphs!)
|
||||
|
||||
(defn stop-upload!
|
||||
[repo]
|
||||
(sync-apply/set-upload-stopped! repo true))
|
||||
|
||||
(defn resume-upload!
|
||||
[repo]
|
||||
(sync-apply/set-upload-stopped! repo false))
|
||||
|
||||
(defn upload-stopped?
|
||||
[repo]
|
||||
(sync-apply/upload-stopped? repo))
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
(ns frontend.worker.sync.client-op
|
||||
"Store client-ops in a persisted datascript"
|
||||
"Store client sync metadata and ops in sqlite tables.
|
||||
DataScript client-op storage is deprecated and unsupported."
|
||||
(:require [datascript.core :as d]
|
||||
[frontend.worker.state :as worker-state]
|
||||
[goog.object :as gobj]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.sqlite.util :as sqlite-util]
|
||||
[malli.core :as ma]
|
||||
[malli.transform :as mt]))
|
||||
|
||||
@@ -29,85 +31,255 @@
|
||||
|
||||
(defonce *repo->pending-local-tx-count (atom {}))
|
||||
|
||||
(def schema-in-db
|
||||
"TODO: rename this db-name from client-op to client-metadata+op.
|
||||
and move it to its own namespace."
|
||||
{:block/uuid {:db/unique :db.unique/identity}
|
||||
:db-ident {:db/unique :db.unique/identity}
|
||||
:db-ident-or-block-uuid {:db/unique :db.unique/identity}
|
||||
;; local-tx is the latest remote-tx that local db persists
|
||||
:local-tx {:db/index true}
|
||||
:graph-uuid {:db/index true}
|
||||
:db-sync/checksum {:db/index true}
|
||||
:db-sync/tx-id {:db/unique :db.unique/identity}
|
||||
:db-sync/created-at {:db/index true}
|
||||
:db-sync/pending? {:db/index true}
|
||||
:db-sync/outliner-op {}
|
||||
:db-sync/forward-outliner-ops {}
|
||||
:db-sync/inverse-outliner-ops {}
|
||||
:db-sync/inferred-outliner-ops? {}
|
||||
:db-sync/normalized-tx-data {}
|
||||
:db-sync/reversed-tx-data {}
|
||||
:db-sync/asset-op? {:db/index true}})
|
||||
(def ^:private sqlite-schema-ready-key "__logseq_client_ops_schema_ready")
|
||||
(def ^:private sqlite-mode-key "__logseq_client_ops_sqlite_mode")
|
||||
(def ^:private sync-meta-table-sql
|
||||
"create table if not exists sync_meta (key text primary key, value text)")
|
||||
(def ^:private client-ops-table-sql
|
||||
(str "create table if not exists client_ops ("
|
||||
"id integer primary key autoincrement,"
|
||||
"kind text not null,"
|
||||
"created_at integer not null,"
|
||||
"tx_id text unique,"
|
||||
"pending integer not null default 0,"
|
||||
"failed integer not null default 0,"
|
||||
"outliner_op text,"
|
||||
"undo_redo text,"
|
||||
"forward_outliner_ops text,"
|
||||
"inverse_outliner_ops text,"
|
||||
"inferred_outliner_ops integer,"
|
||||
"normalized_tx_data text,"
|
||||
"reversed_tx_data text,"
|
||||
"asset_uuid text,"
|
||||
"asset_op text,"
|
||||
"asset_t integer,"
|
||||
"asset_value text"
|
||||
")"))
|
||||
(def ^:private pending-index-sql
|
||||
"create index if not exists idx_client_ops_pending_created on client_ops(kind, pending, created_at, id)")
|
||||
(def ^:private asset-index-sql
|
||||
"create index if not exists idx_client_ops_asset_uuid on client_ops(kind, asset_uuid)")
|
||||
|
||||
(defn- client-ops-store
|
||||
[repo]
|
||||
(worker-state/get-client-ops-conn repo))
|
||||
|
||||
(declare ensure-sqlite-schema!)
|
||||
|
||||
(defn- detect-sqlite-mode
|
||||
[^js db]
|
||||
(or (gobj/get db sqlite-mode-key)
|
||||
(let [mode
|
||||
(cond
|
||||
(not db) nil
|
||||
(fn? (gobj/get db "prepare"))
|
||||
(try
|
||||
(let [^js stmt (.prepare db "select 1")]
|
||||
(try
|
||||
(if (fn? (gobj/get stmt "run"))
|
||||
:better-sqlite
|
||||
:sqlite-wasm)
|
||||
(finally
|
||||
(when (fn? (gobj/get stmt "finalize"))
|
||||
(.finalize stmt)))))
|
||||
(catch :default _
|
||||
:sqlite-wasm))
|
||||
(fn? (gobj/get db "exec")) :sqlite-wasm
|
||||
:else nil)]
|
||||
(when mode
|
||||
(try
|
||||
(gobj/set db sqlite-mode-key mode)
|
||||
(catch :default _
|
||||
nil)))
|
||||
mode)))
|
||||
|
||||
(defn- sqlite-db?
|
||||
[conn]
|
||||
(some? (detect-sqlite-mode conn)))
|
||||
|
||||
(defn- sqlite-store-or-throw
|
||||
[repo]
|
||||
(when-let [store (client-ops-store repo)]
|
||||
(if (sqlite-db? store)
|
||||
(do
|
||||
(ensure-sqlite-schema! store)
|
||||
store)
|
||||
(throw (ex-info "Legacy DataScript client-op storage is unsupported. Please back up the graph and re-download it."
|
||||
{:type :db-sync/legacy-client-ops-storage
|
||||
:repo repo})))))
|
||||
|
||||
(defn- parse-uuid-str
|
||||
[v]
|
||||
(when (string? v)
|
||||
(try
|
||||
(uuid v)
|
||||
(catch :default _
|
||||
nil))))
|
||||
|
||||
(defn- kw->str
|
||||
[v]
|
||||
(cond
|
||||
(keyword? v) (name v)
|
||||
(string? v) v
|
||||
:else nil))
|
||||
|
||||
(defn- str->kw
|
||||
[v]
|
||||
(when (string? v)
|
||||
(keyword v)))
|
||||
|
||||
(defn- bool->int [v] (if v 1 0))
|
||||
(defn- int->bool [v] (not (or (nil? v) (= 0 v) (= false v))))
|
||||
|
||||
(defn- normalize-op-entries
|
||||
[ops]
|
||||
(let [ops' (some-> ops seq vec)]
|
||||
(cond
|
||||
(nil? ops')
|
||||
nil
|
||||
|
||||
(and (keyword? (first ops'))
|
||||
(vector? (second ops')))
|
||||
[ops']
|
||||
|
||||
:else
|
||||
ops')))
|
||||
|
||||
(defn- sqlite-run!
|
||||
[^js db sql params]
|
||||
(case (detect-sqlite-mode db)
|
||||
:better-sqlite
|
||||
(let [^js stmt (.prepare db sql)
|
||||
run-fn (gobj/get stmt "run")]
|
||||
(if (seq params)
|
||||
(.apply run-fn stmt (to-array params))
|
||||
(.call run-fn stmt)))
|
||||
|
||||
:sqlite-wasm
|
||||
(.exec db #js {:sql sql
|
||||
:bind (into-array params)})
|
||||
|
||||
nil))
|
||||
|
||||
(defn- sqlite-rows
|
||||
[^js db sql params]
|
||||
(case (detect-sqlite-mode db)
|
||||
:better-sqlite
|
||||
(let [^js stmt (.prepare db sql)
|
||||
all-fn (gobj/get stmt "all")]
|
||||
(vec (if (seq params)
|
||||
(.apply all-fn stmt (to-array params))
|
||||
(.call all-fn stmt))))
|
||||
|
||||
:sqlite-wasm
|
||||
(let [^js result (.exec db #js {:sql sql
|
||||
:bind (into-array params)
|
||||
:rowMode "object"
|
||||
:returnValue "resultRows"})]
|
||||
(cond
|
||||
(nil? result) []
|
||||
(array? result) (vec result)
|
||||
(fn? (gobj/get result "toArray")) (vec (.toArray result))
|
||||
:else []))
|
||||
|
||||
[]))
|
||||
|
||||
(defn- sqlite-row
|
||||
[db sql params]
|
||||
(first (sqlite-rows db sql params)))
|
||||
|
||||
(defn- sqlite-with-tx!
|
||||
[^js db f]
|
||||
(case (detect-sqlite-mode db)
|
||||
:better-sqlite
|
||||
(let [tx-fn (.transaction db (fn [] (f db)))]
|
||||
(tx-fn))
|
||||
|
||||
:sqlite-wasm
|
||||
(if (fn? (gobj/get db "transaction"))
|
||||
(.transaction db (fn [tx] (f tx)))
|
||||
(f db))
|
||||
|
||||
(f db)))
|
||||
|
||||
(defn ensure-sqlite-schema!
|
||||
[db]
|
||||
(when (sqlite-db? db)
|
||||
(when-not (true? (gobj/get db sqlite-schema-ready-key))
|
||||
(sqlite-with-tx!
|
||||
db
|
||||
(fn [tx]
|
||||
(sqlite-run! tx sync-meta-table-sql [])
|
||||
(sqlite-run! tx client-ops-table-sql [])
|
||||
(sqlite-run! tx pending-index-sql [])
|
||||
(sqlite-run! tx asset-index-sql [])))
|
||||
(try
|
||||
(gobj/set db sqlite-schema-ready-key true)
|
||||
(catch :default _
|
||||
nil)))))
|
||||
|
||||
(defn- sqlite-get-meta
|
||||
[db k]
|
||||
(some-> (sqlite-row db "select value from sync_meta where key = ?" [(name k)])
|
||||
(aget "value")))
|
||||
|
||||
(defn- sqlite-set-meta!
|
||||
[db k v]
|
||||
(sqlite-run! db
|
||||
(str "insert into sync_meta (key, value) values (?, ?)"
|
||||
" on conflict(key) do update set value = excluded.value")
|
||||
[(name k) (str v)]))
|
||||
|
||||
(defn- sqlite-delete-meta!
|
||||
[db k]
|
||||
(sqlite-run! db "delete from sync_meta where key = ?" [(name k)]))
|
||||
|
||||
(defn update-graph-uuid
|
||||
[repo graph-uuid]
|
||||
{:pre [(some? graph-uuid)]}
|
||||
(when-let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(let [old-datoms (d/datoms @conn :avet :graph-uuid)
|
||||
retractions (mapv (fn [datom]
|
||||
[:db/retract (:e datom) :graph-uuid (:v datom)])
|
||||
old-datoms)]
|
||||
(ldb/transact! conn (conj retractions [:db/add "e" :graph-uuid graph-uuid])))))
|
||||
(when-let [store (sqlite-store-or-throw repo)]
|
||||
(sqlite-set-meta! store :graph-uuid graph-uuid)))
|
||||
|
||||
(defn get-graph-uuid
|
||||
[repo]
|
||||
(when-let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(:v (first (d/datoms @conn :avet :graph-uuid)))))
|
||||
(some-> (sqlite-store-or-throw repo)
|
||||
(sqlite-get-meta :graph-uuid)))
|
||||
|
||||
(defn update-local-tx
|
||||
[repo t]
|
||||
{:pre [(some? t)]}
|
||||
(let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(assert (some? conn) repo)
|
||||
(let [tx-data
|
||||
(if-let [datom (first (d/datoms @conn :avet :local-tx))]
|
||||
[:db/add (:e datom) :local-tx t]
|
||||
(if-let [datom (first (d/datoms @conn :avet :db-sync/checksum))]
|
||||
[:db/add (:e datom) :local-tx t]
|
||||
[:db/add "e" :local-tx t]))]
|
||||
(ldb/transact! conn [tx-data]))))
|
||||
(let [store (sqlite-store-or-throw repo)]
|
||||
(assert (some? store) repo)
|
||||
(sqlite-set-meta! store :local-tx t)))
|
||||
|
||||
(defn update-local-checksum
|
||||
[repo checksum]
|
||||
{:pre [(some? checksum)]}
|
||||
(let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(assert (some? conn) repo)
|
||||
(let [tx-data
|
||||
(if-let [datom (first (d/datoms @conn :avet :db-sync/checksum))]
|
||||
[:db/add (:e datom) :db-sync/checksum checksum]
|
||||
(if-let [datom (first (d/datoms @conn :avet :local-tx))]
|
||||
[:db/add (:e datom) :db-sync/checksum checksum]
|
||||
[:db/add "e" :db-sync/checksum checksum]))]
|
||||
(ldb/transact! conn [tx-data]))))
|
||||
(let [store (sqlite-store-or-throw repo)]
|
||||
(assert (some? store) repo)
|
||||
(sqlite-set-meta! store :db-sync/checksum checksum)))
|
||||
|
||||
(defn remove-local-tx
|
||||
[repo]
|
||||
(when-let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(when-let [datom (first (d/datoms @conn :avet :local-tx))]
|
||||
(ldb/transact! conn [[:db/retract (:e datom) :local-tx]]))))
|
||||
(when-let [store (sqlite-store-or-throw repo)]
|
||||
(sqlite-delete-meta! store :local-tx)))
|
||||
|
||||
(defn get-local-tx
|
||||
[repo]
|
||||
(when-let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(:v (first (d/datoms @conn :avet :local-tx)))))
|
||||
(when-let [store (sqlite-store-or-throw repo)]
|
||||
(some-> (sqlite-get-meta store :local-tx)
|
||||
(js/parseInt 10))))
|
||||
|
||||
(defn get-pending-local-tx-count
|
||||
[repo]
|
||||
(if-let [cached (get @*repo->pending-local-tx-count repo)]
|
||||
cached
|
||||
(let [count' (if-let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(count (d/datoms @conn :avet :db-sync/pending? true))
|
||||
(let [count' (if-let [store (sqlite-store-or-throw repo)]
|
||||
(or (some-> (sqlite-row store
|
||||
"select count(*) as c from client_ops where kind = 'tx' and pending = 1"
|
||||
[])
|
||||
(aget "c"))
|
||||
0)
|
||||
0)]
|
||||
(swap! *repo->pending-local-tx-count assoc repo count')
|
||||
count')))
|
||||
@@ -122,9 +294,9 @@
|
||||
|
||||
(defn get-local-checksum
|
||||
[repo]
|
||||
(let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(assert (some? conn) repo)
|
||||
(:v (first (d/datoms @conn :avet :db-sync/checksum)))))
|
||||
(let [store (sqlite-store-or-throw repo)]
|
||||
(assert (some? store) repo)
|
||||
(sqlite-get-meta store :db-sync/checksum)))
|
||||
|
||||
(defn rtc-db-graph?
|
||||
"Is RTC enabled"
|
||||
@@ -132,12 +304,188 @@
|
||||
(or (exists? js/process)
|
||||
(some? (get-graph-uuid repo))))
|
||||
|
||||
(defn- row->pending-local-tx
|
||||
[row]
|
||||
(let [tx-id (parse-uuid-str (aget row "tx_id"))]
|
||||
(when tx-id
|
||||
{:tx-id tx-id
|
||||
:outliner-op (str->kw (aget row "outliner_op"))
|
||||
:forward-outliner-ops (or (normalize-op-entries
|
||||
(sqlite-util/read-transit-str (aget row "forward_outliner_ops")))
|
||||
[])
|
||||
:inverse-outliner-ops (or (normalize-op-entries
|
||||
(sqlite-util/read-transit-str (aget row "inverse_outliner_ops")))
|
||||
[])
|
||||
:inferred-outliner-ops? (int->bool (aget row "inferred_outliner_ops"))
|
||||
:db-sync/undo-redo (str->kw (aget row "undo_redo"))
|
||||
:tx (sqlite-util/read-transit-str (aget row "normalized_tx_data"))
|
||||
:reversed-tx (sqlite-util/read-transit-str (aget row "reversed_tx_data"))})))
|
||||
|
||||
(defn upsert-local-tx-entry!
|
||||
[repo {:keys [tx-id created-at pending? failed? outliner-op undo-redo
|
||||
forward-outliner-ops inverse-outliner-ops inferred-outliner-ops?
|
||||
normalized-tx-data reversed-tx-data]
|
||||
:or {pending? true failed? false}}]
|
||||
{:pre [(some? tx-id)]}
|
||||
(let [store (sqlite-store-or-throw repo)]
|
||||
(assert (some? store) repo)
|
||||
(let [tx-id-str (str tx-id)
|
||||
existing (sqlite-row store
|
||||
"select pending, created_at from client_ops where kind = 'tx' and tx_id = ?"
|
||||
[tx-id-str])
|
||||
should-inc-pending? (not= 1 (some-> existing (aget "pending")))
|
||||
forward-outliner-ops' (or (normalize-op-entries forward-outliner-ops) [])
|
||||
inverse-outliner-ops' (or (normalize-op-entries inverse-outliner-ops) [])
|
||||
created-at' (or (some-> existing (aget "created_at"))
|
||||
created-at
|
||||
(.now js/Date))]
|
||||
(sqlite-run! store
|
||||
(str "insert into client_ops ("
|
||||
"kind, created_at, tx_id, pending, failed, outliner_op, undo_redo, "
|
||||
"forward_outliner_ops, inverse_outliner_ops, inferred_outliner_ops, "
|
||||
"normalized_tx_data, reversed_tx_data"
|
||||
") values ('tx', ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)"
|
||||
" on conflict(tx_id) do update set "
|
||||
"created_at = excluded.created_at,"
|
||||
"pending = excluded.pending,"
|
||||
"failed = excluded.failed,"
|
||||
"outliner_op = excluded.outliner_op,"
|
||||
"undo_redo = excluded.undo_redo,"
|
||||
"forward_outliner_ops = excluded.forward_outliner_ops,"
|
||||
"inverse_outliner_ops = excluded.inverse_outliner_ops,"
|
||||
"inferred_outliner_ops = excluded.inferred_outliner_ops,"
|
||||
"normalized_tx_data = excluded.normalized_tx_data,"
|
||||
"reversed_tx_data = excluded.reversed_tx_data")
|
||||
[created-at'
|
||||
tx-id-str
|
||||
(bool->int pending?)
|
||||
(bool->int failed?)
|
||||
(kw->str outliner-op)
|
||||
(kw->str undo-redo)
|
||||
(sqlite-util/write-transit-str forward-outliner-ops')
|
||||
(sqlite-util/write-transit-str inverse-outliner-ops')
|
||||
(bool->int inferred-outliner-ops?)
|
||||
(sqlite-util/write-transit-str (or normalized-tx-data []))
|
||||
(sqlite-util/write-transit-str (or reversed-tx-data []))])
|
||||
{:tx-id tx-id
|
||||
:created-at created-at'
|
||||
:should-inc-pending? should-inc-pending?})))
|
||||
|
||||
(defn get-local-tx-entry
|
||||
[repo tx-id]
|
||||
(when (uuid? tx-id)
|
||||
(when-let [store (sqlite-store-or-throw repo)]
|
||||
(some-> (sqlite-row store
|
||||
(str "select tx_id, outliner_op, undo_redo, "
|
||||
"forward_outliner_ops, inverse_outliner_ops, inferred_outliner_ops, "
|
||||
"normalized_tx_data, reversed_tx_data "
|
||||
"from client_ops where kind = 'tx' and tx_id = ? limit 1")
|
||||
[(str tx-id)])
|
||||
(row->pending-local-tx)))))
|
||||
|
||||
(defn get-pending-local-txs
|
||||
[repo & {:keys [limit]}]
|
||||
(when-let [store (sqlite-store-or-throw repo)]
|
||||
(let [sql (str "select tx_id, outliner_op, undo_redo, "
|
||||
"forward_outliner_ops, inverse_outliner_ops, inferred_outliner_ops, "
|
||||
"normalized_tx_data, reversed_tx_data "
|
||||
"from client_ops where kind = 'tx' and pending = 1 "
|
||||
"order by created_at asc, id asc"
|
||||
(when (number? limit) " limit ?"))
|
||||
rows (sqlite-rows store sql (if (number? limit) [limit] []))]
|
||||
(->> rows
|
||||
(keep row->pending-local-tx)
|
||||
vec))))
|
||||
|
||||
(defn- pending-tx-id?
|
||||
[store tx-id]
|
||||
(let [row (sqlite-row store
|
||||
"select pending from client_ops where kind = 'tx' and tx_id = ?"
|
||||
[(str tx-id)])]
|
||||
(= 1 (some-> row (aget "pending")))))
|
||||
|
||||
(defn mark-pending-txs-false!
|
||||
[repo tx-ids]
|
||||
(when-let [store (sqlite-store-or-throw repo)]
|
||||
(let [tx-ids (->> tx-ids (filter uuid?) vec)
|
||||
pending-to-remove (->> tx-ids
|
||||
(filter (fn [tx-id]
|
||||
(pending-tx-id? store tx-id)))
|
||||
count)]
|
||||
(when (seq tx-ids)
|
||||
(doseq [tx-id tx-ids]
|
||||
(sqlite-run! store
|
||||
"update client_ops set pending = 0 where kind = 'tx' and tx_id = ?"
|
||||
[(str tx-id)])))
|
||||
pending-to-remove)))
|
||||
|
||||
(defn mark-failed-txs!
|
||||
[repo tx-ids]
|
||||
(when-let [store (sqlite-store-or-throw repo)]
|
||||
(let [tx-ids (->> tx-ids (filter uuid?) vec)
|
||||
pending-to-remove (->> tx-ids
|
||||
(filter (fn [tx-id]
|
||||
(pending-tx-id? store tx-id)))
|
||||
count)]
|
||||
(when (seq tx-ids)
|
||||
(doseq [tx-id tx-ids]
|
||||
(sqlite-run! store
|
||||
"update client_ops set pending = 0, failed = 1 where kind = 'tx' and tx_id = ?"
|
||||
[(str tx-id)])))
|
||||
pending-to-remove)))
|
||||
|
||||
(defn history-action-ops-by-tx-id
|
||||
[repo tx-id]
|
||||
(when-let [entry (get-local-tx-entry repo tx-id)]
|
||||
{:db-sync/forward-outliner-ops (some-> (:forward-outliner-ops entry) seq vec)
|
||||
:db-sync/inverse-outliner-ops (some-> (:inverse-outliner-ops entry) seq vec)}))
|
||||
|
||||
(defn- local-asset-op-map
|
||||
[op-type t value]
|
||||
(let [asset-uuid (:block-uuid value)]
|
||||
(case op-type
|
||||
:update-asset {:block/uuid asset-uuid
|
||||
:update-asset [:update-asset t value]}
|
||||
:remove-asset {:block/uuid asset-uuid
|
||||
:remove-asset [:remove-asset t value]}
|
||||
nil)))
|
||||
|
||||
(defn- sqlite-asset-op-by-uuid
|
||||
[store block-uuid]
|
||||
(when-let [row (sqlite-row store
|
||||
(str "select asset_uuid, asset_op, asset_t, asset_value "
|
||||
"from client_ops where kind = 'asset' and asset_uuid = ? limit 1")
|
||||
[(str block-uuid)])]
|
||||
(let [op-type (str->kw (aget row "asset_op"))
|
||||
t (aget row "asset_t")
|
||||
value (or (some-> (aget row "asset_value") sqlite-util/read-transit-str)
|
||||
{:block-uuid block-uuid})]
|
||||
(local-asset-op-map op-type t value))))
|
||||
|
||||
(defn- sqlite-upsert-asset-op!
|
||||
[store op-type t value]
|
||||
(let [block-uuid (:block-uuid value)]
|
||||
(sqlite-with-tx!
|
||||
store
|
||||
(fn [tx]
|
||||
(sqlite-run! tx "delete from client_ops where kind = 'asset' and asset_uuid = ?"
|
||||
[(str block-uuid)])
|
||||
(sqlite-run! tx
|
||||
(str "insert into client_ops ("
|
||||
"kind, created_at, asset_uuid, asset_op, asset_t, asset_value"
|
||||
") values ('asset', ?, ?, ?, ?, ?)")
|
||||
[(.now js/Date)
|
||||
(str block-uuid)
|
||||
(kw->str op-type)
|
||||
t
|
||||
(sqlite-util/write-transit-str value)])))))
|
||||
|
||||
;;; asset ops
|
||||
(defn add-asset-ops
|
||||
[repo asset-ops]
|
||||
(let [conn (worker-state/get-client-ops-conn repo)
|
||||
(let [store (sqlite-store-or-throw repo)
|
||||
ops (ops-coercer asset-ops)]
|
||||
(assert (some? conn) repo)
|
||||
(assert (some? store) repo)
|
||||
(letfn [(already-removed? [remove-op t]
|
||||
(some-> remove-op second (> t)))
|
||||
(update-after-remove? [update-op t]
|
||||
@@ -145,26 +493,20 @@
|
||||
(doseq [op ops]
|
||||
(let [[op-type t value] op
|
||||
{:keys [block-uuid]} value
|
||||
exist-block-ops-entity (d/entity @conn [:block/uuid block-uuid])
|
||||
e (:db/id exist-block-ops-entity)]
|
||||
(when-let [tx-data
|
||||
(not-empty
|
||||
(case op-type
|
||||
:update-asset
|
||||
(let [remove-asset-op (get exist-block-ops-entity :remove-asset)]
|
||||
(when-not (already-removed? remove-asset-op t)
|
||||
(cond-> [{:block/uuid block-uuid
|
||||
:db-sync/asset-op? true
|
||||
:update-asset op}]
|
||||
remove-asset-op (conj [:db.fn/retractAttribute e :remove-asset]))))
|
||||
:remove-asset
|
||||
(let [update-asset-op (get exist-block-ops-entity :update-asset)]
|
||||
(when-not (update-after-remove? update-asset-op t)
|
||||
(cond-> [{:block/uuid block-uuid
|
||||
:db-sync/asset-op? true
|
||||
:remove-asset op}]
|
||||
update-asset-op (conj [:db.fn/retractAttribute e :update-asset]))))))]
|
||||
(ldb/transact! conn tx-data)))))))
|
||||
existing-op (sqlite-asset-op-by-uuid store block-uuid)]
|
||||
(case op-type
|
||||
:update-asset
|
||||
(let [remove-asset-op (:remove-asset existing-op)]
|
||||
(when-not (already-removed? remove-asset-op t)
|
||||
(sqlite-upsert-asset-op! store :update-asset t value)))
|
||||
|
||||
:remove-asset
|
||||
(let [update-asset-op (:update-asset existing-op)]
|
||||
(when-not (update-after-remove? update-asset-op t)
|
||||
(sqlite-upsert-asset-op! store :remove-asset t value)))
|
||||
|
||||
nil)))
|
||||
nil)))
|
||||
|
||||
(defn add-all-exists-asset-as-ops
|
||||
[repo]
|
||||
@@ -172,54 +514,61 @@
|
||||
_ (assert (some? conn))
|
||||
asset-block-uuids (->> (d/datoms @conn :avet :logseq.property.asset/type)
|
||||
(keep (fn [d]
|
||||
(:block/uuid (d/entity @conn (:e d))))))
|
||||
ops (map
|
||||
(fn [block-uuid] [:update-asset 1 {:block-uuid block-uuid}])
|
||||
asset-block-uuids)]
|
||||
(:block/uuid (d/entity @conn (:e d)))))
|
||||
distinct)
|
||||
ops (map (fn [block-uuid] [:update-asset 1 {:block-uuid block-uuid}])
|
||||
asset-block-uuids)]
|
||||
(add-asset-ops repo ops)))
|
||||
|
||||
(defn- get-all-asset-ops*
|
||||
[db]
|
||||
(->> (d/datoms db :avet :db-sync/asset-op?)
|
||||
(map (fn [d]
|
||||
(let [op (d/entity db (:e d))]
|
||||
[(:e d) (into {} op)])))
|
||||
(into {})))
|
||||
|
||||
(defn get-unpushed-asset-ops-count
|
||||
[repo]
|
||||
(when-let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(count (get-all-asset-ops* @conn))))
|
||||
(when-let [store (sqlite-store-or-throw repo)]
|
||||
(or (some-> (sqlite-row store
|
||||
"select count(*) as c from client_ops where kind = 'asset'"
|
||||
[])
|
||||
(aget "c"))
|
||||
0)))
|
||||
|
||||
(defn get-all-asset-ops
|
||||
[repo]
|
||||
(when-let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(vals (get-all-asset-ops* @conn))))
|
||||
(when-let [store (sqlite-store-or-throw repo)]
|
||||
(->> (sqlite-rows store
|
||||
"select asset_op, asset_t, asset_value from client_ops where kind = 'asset' order by id asc"
|
||||
[])
|
||||
(keep (fn [row]
|
||||
(let [op-type (str->kw (aget row "asset_op"))
|
||||
t (aget row "asset_t")
|
||||
value (some-> (aget row "asset_value") sqlite-util/read-transit-str)]
|
||||
(when (and op-type (map? value) (:block-uuid value))
|
||||
(local-asset-op-map op-type t value)))))
|
||||
vec)))
|
||||
|
||||
(defn remove-asset-op
|
||||
[repo asset-uuid]
|
||||
(when-let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(let [ent (d/entity @conn [:block/uuid asset-uuid])]
|
||||
(when-let [e (:db/id ent)]
|
||||
(ldb/transact! conn [[:db/retractEntity e]])))))
|
||||
(when-let [store (sqlite-store-or-throw repo)]
|
||||
(sqlite-run! store
|
||||
"delete from client_ops where kind = 'asset' and asset_uuid = ?"
|
||||
[(str asset-uuid)])))
|
||||
|
||||
(defn cleanup-finished-history-ops!
|
||||
[repo protected-tx-ids]
|
||||
(if-let [conn (worker-state/get-client-ops-conn repo)]
|
||||
(if-let [store (sqlite-store-or-throw repo)]
|
||||
(let [protected-tx-ids (set protected-tx-ids)
|
||||
tx-ent-ids (->> (d/datoms @conn :avet :db-sync/tx-id)
|
||||
(keep (fn [datom]
|
||||
(let [tx-id (:v datom)
|
||||
ent (d/entity @conn (:e datom))]
|
||||
(when (and (uuid? tx-id)
|
||||
(false? (:db-sync/pending? ent))
|
||||
(not (contains? protected-tx-ids tx-id)))
|
||||
(:db/id ent)))))
|
||||
vec)]
|
||||
(when (seq tx-ent-ids)
|
||||
(ldb/transact! conn
|
||||
(mapv (fn [ent-id]
|
||||
[:db/retractEntity ent-id])
|
||||
tx-ent-ids)))
|
||||
(count tx-ent-ids))
|
||||
tx-id-rows (sqlite-rows store
|
||||
(str "select tx_id from client_ops "
|
||||
"where kind = 'tx' and pending = 0 and tx_id is not null")
|
||||
[])
|
||||
removable-tx-ids (->> tx-id-rows
|
||||
(keep (fn [row]
|
||||
(let [tx-id (parse-uuid-str (aget row "tx_id"))]
|
||||
(when (and (uuid? tx-id)
|
||||
(not (contains? protected-tx-ids tx-id)))
|
||||
tx-id))))
|
||||
vec)]
|
||||
(when (seq removable-tx-ids)
|
||||
(doseq [tx-id removable-tx-ids]
|
||||
(sqlite-run! store
|
||||
"delete from client_ops where kind = 'tx' and tx_id = ?"
|
||||
[(str tx-id)])))
|
||||
(count removable-tx-ids))
|
||||
0))
|
||||
|
||||
@@ -25,6 +25,13 @@
|
||||
[platform']
|
||||
(get-in platform' [:env :runtime]))
|
||||
|
||||
(defn- read-transit-safe
|
||||
[value]
|
||||
(try
|
||||
(ldb/read-transit-str value)
|
||||
(catch :default _
|
||||
invalid-transit)))
|
||||
|
||||
(defn- browser-runtime?
|
||||
[platform']
|
||||
(= :browser (runtime platform')))
|
||||
@@ -569,12 +576,17 @@
|
||||
(defn <decrypt-text-value
|
||||
[aes-key value]
|
||||
(assert (string? value) (str "encrypted value should be a string, value: " value))
|
||||
(let [decoded (ldb/read-transit-str value)]
|
||||
(let [decoded (read-transit-safe value)]
|
||||
(if (= decoded invalid-transit)
|
||||
(p/resolved value)
|
||||
(p/let [value (crypt/<decrypt-text-if-encrypted aes-key decoded)
|
||||
value' (ldb/read-transit-str value)]
|
||||
value'))))
|
||||
(p/let [value (or (crypt/<decrypt-text-if-encrypted aes-key decoded)
|
||||
decoded)
|
||||
value' (if (string? value)
|
||||
(read-transit-safe value)
|
||||
value)]
|
||||
(if (= value' invalid-transit)
|
||||
value
|
||||
value')))))
|
||||
|
||||
(defn- encrypt-tx-item
|
||||
[aes-key item]
|
||||
|
||||
@@ -60,22 +60,53 @@
|
||||
(<decompress-gzip-bytes chunk)
|
||||
chunk)))
|
||||
|
||||
(defn- response-body-stream
|
||||
[^js resp]
|
||||
(let [encoding (some-> resp .-headers (.get "content-encoding"))]
|
||||
(cond
|
||||
(nil? (.-body resp))
|
||||
nil
|
||||
(defn- <stream-starts-with-gzip?
|
||||
[^js stream]
|
||||
(let [reader (.getReader stream)]
|
||||
(-> (.read reader)
|
||||
(p/then (fn [result]
|
||||
(if (.-done result)
|
||||
false
|
||||
(gzip-bytes? (->uint8 (.-value result))))))
|
||||
(p/catch (fn [_] false))
|
||||
(p/finally (fn []
|
||||
(try
|
||||
(.releaseLock reader)
|
||||
(catch :default _)))))))
|
||||
|
||||
;; NOTE: Some runtimes (e.g. Node fetch) may auto-decompress while still
|
||||
;; keeping `content-encoding: gzip` in headers. Avoid stream-level manual
|
||||
;; decompression here to prevent double-decompression failures. For gzip,
|
||||
;; fall back to arrayBuffer path where we detect gzip by magic bytes.
|
||||
(defn- <response-body-stream
|
||||
[^js resp]
|
||||
(let [body (.-body resp)
|
||||
encoding (some-> resp .-headers (.get "content-encoding"))]
|
||||
(cond
|
||||
(nil? body)
|
||||
(p/resolved nil)
|
||||
|
||||
;; Never trust `content-encoding` alone.
|
||||
;; Some runtimes (e.g. Node/undici fetch) may auto-decompress body while
|
||||
;; still exposing `content-encoding: gzip` in headers.
|
||||
;; We only stream-decompress when the first bytes are actual gzip magic.
|
||||
(and (= "gzip" encoding)
|
||||
(exists? js/DecompressionStream)
|
||||
(fn? (.-tee body)))
|
||||
(let [branches (.tee body)
|
||||
probe (aget branches 0)
|
||||
payload (aget branches 1)]
|
||||
(-> (<stream-starts-with-gzip? probe)
|
||||
(p/then (fn [gzip?]
|
||||
(if gzip?
|
||||
(.pipeThrough payload (js/DecompressionStream. "gzip"))
|
||||
payload)))
|
||||
;; If probing fails, keep original payload stream.
|
||||
(p/catch (fn [_] payload))))
|
||||
|
||||
;; If we cannot safely probe (no tee support), do not guess.
|
||||
;; Fall back to the arrayBuffer path where we inspect magic bytes first.
|
||||
(= "gzip" encoding)
|
||||
nil
|
||||
(p/resolved nil)
|
||||
|
||||
:else
|
||||
(.-body resp))))
|
||||
(p/resolved body))))
|
||||
|
||||
(defn- <flush-row-batches!
|
||||
[rows batch-size on-batch]
|
||||
@@ -89,29 +120,30 @@
|
||||
|
||||
(defn- <stream-snapshot-row-batches!
|
||||
[^js resp batch-size on-batch]
|
||||
(if-let [stream (response-body-stream resp)]
|
||||
(let [reader (.getReader stream)]
|
||||
(p/loop [buffer nil
|
||||
pending []]
|
||||
(p/let [result (.read reader)]
|
||||
(if (.-done result)
|
||||
(let [pending (if (and buffer (pos? (.-byteLength buffer)))
|
||||
(into pending (snapshot/finalize-framed-buffer buffer))
|
||||
pending)]
|
||||
(if (seq pending)
|
||||
(p/let [_ (on-batch pending)]
|
||||
{:chunk-count 1})
|
||||
{:chunk-count 0}))
|
||||
(let [{rows :rows next-buffer :buffer} (snapshot/parse-framed-chunk buffer (->uint8 (.-value result)))
|
||||
pending (into pending rows)]
|
||||
(p/let [pending (<flush-row-batches! pending batch-size on-batch)]
|
||||
(p/recur next-buffer pending)))))))
|
||||
(p/let [snapshot-bytes (<snapshot-response-bytes resp)
|
||||
rows (vec (snapshot/finalize-framed-buffer snapshot-bytes))]
|
||||
(if (seq rows)
|
||||
(p/let [_ (on-batch rows)]
|
||||
{:chunk-count 1})
|
||||
{:chunk-count 0}))))
|
||||
(p/let [stream (<response-body-stream resp)]
|
||||
(if stream
|
||||
(let [reader (.getReader stream)]
|
||||
(p/loop [buffer nil
|
||||
pending []]
|
||||
(p/let [result (.read reader)]
|
||||
(if (.-done result)
|
||||
(let [pending (if (and buffer (pos? (.-byteLength buffer)))
|
||||
(into pending (snapshot/finalize-framed-buffer buffer))
|
||||
pending)]
|
||||
(if (seq pending)
|
||||
(p/let [_ (on-batch pending)]
|
||||
{:chunk-count 1})
|
||||
{:chunk-count 0}))
|
||||
(let [{rows :rows next-buffer :buffer} (snapshot/parse-framed-chunk buffer (->uint8 (.-value result)))
|
||||
pending (into pending rows)]
|
||||
(p/let [pending (<flush-row-batches! pending batch-size on-batch)]
|
||||
(p/recur next-buffer pending)))))))
|
||||
(p/let [snapshot-bytes (<snapshot-response-bytes resp)
|
||||
rows (vec (snapshot/finalize-framed-buffer snapshot-bytes))]
|
||||
(if (seq rows)
|
||||
(p/let [_ (on-batch rows)]
|
||||
{:chunk-count 1})
|
||||
{:chunk-count 0})))))
|
||||
|
||||
(defn- with-auth-headers
|
||||
[opts]
|
||||
@@ -184,6 +216,14 @@
|
||||
(close-import-state! state)
|
||||
(reset! *import-state nil))))
|
||||
|
||||
(defn close-import-state-for-repo!
|
||||
[repo]
|
||||
(when-let [state @*import-state]
|
||||
(when (= repo (:repo state))
|
||||
(close-import-state! state)
|
||||
(reset! *import-state nil)))
|
||||
nil)
|
||||
|
||||
(defn- require-import-state!
|
||||
[repo graph-id import-id]
|
||||
(let [state @*import-state]
|
||||
@@ -262,13 +302,13 @@
|
||||
(p/let [datoms-batch (if graph-e2ee?
|
||||
(sync-crypt/<decrypt-snapshot-datoms-batch aes-key datoms)
|
||||
datoms)
|
||||
ident-tx-data (into [] (comp (filter #(= :db/ident (:a %)))
|
||||
(map datom->tx))
|
||||
datoms-batch)
|
||||
regular-tx-data (into [] (comp (remove #(= :db/ident (:a %)))
|
||||
schema-tx-data (into [] (comp (filter #(= "db" (namespace (:a %))))
|
||||
(map datom->tx))
|
||||
datoms-batch)
|
||||
regular-tx-data (into [] (comp (remove #(= "db" (namespace (:a %))))
|
||||
(map datom->tx))
|
||||
datoms-batch)
|
||||
tx-data (into ident-tx-data regular-tx-data)]
|
||||
tx-data (into schema-tx-data regular-tx-data)]
|
||||
(when (seq tx-data)
|
||||
(d/transact! conn tx-data {:sync-download-graph? true}))))
|
||||
|
||||
@@ -346,6 +386,7 @@
|
||||
[repo reset? graph-id graph-e2ee? & [total-datoms]]
|
||||
(let [graph-e2ee? (if (nil? graph-e2ee?) true (true? graph-e2ee?))]
|
||||
(-> (p/let [close-db-f (require-thread-api-f! :thread-api/db-sync-close-db)
|
||||
unlink-db-f (require-thread-api-f! :thread-api/unsafe-unlink-db)
|
||||
invalidate-search-db-f (require-thread-api-f! :thread-api/db-sync-invalidate-search-db)
|
||||
create-or-open-db-f (require-thread-api-f! :thread-api/create-or-open-db)
|
||||
_ (when-let [state @*import-state]
|
||||
@@ -353,6 +394,7 @@
|
||||
(close-db-f (:repo state)))
|
||||
_ (reset! *import-state nil)
|
||||
_ (when reset? (close-db-f repo))
|
||||
_ (when reset? (unlink-db-f repo))
|
||||
_ (when reset? (invalidate-search-db-f repo))
|
||||
import-id (str (random-uuid))
|
||||
aes-key (when graph-e2ee?
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
(ns frontend.worker.sync.handle-message
|
||||
"WebSocket message handlers for db sync."
|
||||
(:require [datascript.core :as d]
|
||||
[frontend.worker.shared-service :as shared-service]
|
||||
(:require [frontend.worker.shared-service :as shared-service]
|
||||
[frontend.worker.state :as worker-state]
|
||||
[frontend.worker.sync.apply-txs :as sync-apply]
|
||||
[frontend.worker.sync.assets :as sync-assets]
|
||||
[frontend.worker.sync.auth :as sync-auth]
|
||||
[frontend.worker.sync.client-op :as client-op]
|
||||
[frontend.worker.sync.crypt :as sync-crypt]
|
||||
[frontend.worker.sync.log-and-state :as sync-log-state]
|
||||
[frontend.worker.sync.presence :as sync-presence]
|
||||
[frontend.worker.sync.transport :as sync-transport]
|
||||
[frontend.worker.sync.util :as sync-util]
|
||||
@@ -21,10 +21,6 @@
|
||||
(log/error tag data)
|
||||
(throw (ex-info (name tag) data)))
|
||||
|
||||
(defn- client-ops-conn
|
||||
[repo]
|
||||
(sync-presence/client-ops-conn worker-state/get-client-ops-conn repo))
|
||||
|
||||
(defn- sync-counts
|
||||
[repo]
|
||||
(sync-presence/sync-counts
|
||||
@@ -73,6 +69,20 @@
|
||||
(fn [prev]
|
||||
(p/then prev (fn [_] (task)))))))
|
||||
|
||||
(defn- enqueue-send-task!
|
||||
[client task]
|
||||
(if-let [queue (:send-queue client)]
|
||||
(swap! queue
|
||||
(fn [prev]
|
||||
(-> (or prev (p/resolved nil))
|
||||
(p/catch (fn [_] nil))
|
||||
(p/then (fn [_] (task)))
|
||||
(p/catch (fn [error]
|
||||
(log/error :db-sync/send-queue-task-failed
|
||||
{:repo (:repo client)
|
||||
:error error}))))))
|
||||
(task)))
|
||||
|
||||
(defn- current-client
|
||||
[repo]
|
||||
(sync-presence/current-client worker-state/*db-sync-client repo))
|
||||
@@ -93,18 +103,37 @@
|
||||
(when-not (sequential? value)
|
||||
(fail-fast :db-sync/invalid-field (assoc context :value value))))
|
||||
|
||||
(defn- require-uuid
|
||||
[value context]
|
||||
(when-not (uuid? value)
|
||||
(fail-fast :db-sync/invalid-field (assoc context :value value))))
|
||||
|
||||
(defn- parse-transit
|
||||
[value context]
|
||||
(sync-transport/parse-transit fail-fast value context))
|
||||
|
||||
(defn- request-pull!
|
||||
[client since]
|
||||
(when (and (:ws client) (ws-open? (:ws client)))
|
||||
(enqueue-send-task!
|
||||
client
|
||||
(fn []
|
||||
(when (and (:ws client) (ws-open? (:ws client)))
|
||||
(if-let [*pending (:pending-pull-since client)]
|
||||
(let [pending @*pending]
|
||||
(when (or (nil? pending) (< since pending))
|
||||
(reset! *pending since)
|
||||
(send! (:ws client) {:type "pull" :since since})))
|
||||
(send! (:ws client) {:type "pull" :since since})))))))
|
||||
|
||||
(defn- clear-pending-pull!
|
||||
[client]
|
||||
(when-let [*pending (:pending-pull-since client)]
|
||||
(reset! *pending nil)))
|
||||
|
||||
(defn- pending-local-tx?
|
||||
[repo]
|
||||
(when-let [conn (client-ops-conn repo)]
|
||||
(boolean
|
||||
(some (fn [datom]
|
||||
(let [ent (d/entity @conn (:e datom))]
|
||||
(not= false (:db-sync/pending? ent))))
|
||||
(d/datoms @conn :avet :db-sync/created-at)))))
|
||||
(pos? (or (client-op/get-pending-local-tx-count repo) 0)))
|
||||
|
||||
(defn- checksum-compare-ready?
|
||||
[repo client local-t remote-t]
|
||||
@@ -124,48 +153,77 @@
|
||||
|
||||
(defn- verify-sync-checksum!
|
||||
[repo client local-tx remote-tx remote-checksum context]
|
||||
(when (and (string? remote-checksum)
|
||||
(checksum-compare-ready? repo client local-tx remote-tx))
|
||||
(let [local-checksum (local-sync-checksum repo)]
|
||||
(when-not (= local-checksum remote-checksum)
|
||||
(when worker-util/dev?
|
||||
(log/warn :db-sync/checksum-mismatch
|
||||
(merge context
|
||||
{:type :db-sync/checksum-mismatch
|
||||
:repo repo
|
||||
:message-type (:type context)
|
||||
:local-tx local-tx
|
||||
:remote-tx remote-tx
|
||||
:local-checksum local-checksum
|
||||
:remote-checksum remote-checksum})))))))
|
||||
(when worker-util/dev-or-test?
|
||||
(when (and (string? remote-checksum)
|
||||
(checksum-compare-ready? repo client local-tx remote-tx))
|
||||
(let [local-checksum (local-sync-checksum repo)]
|
||||
(when-not (= local-checksum remote-checksum)
|
||||
(let [mismatch-data (merge context
|
||||
{:type :db-sync/checksum-mismatch
|
||||
:repo repo
|
||||
:message-type (:type context)
|
||||
:local-tx local-tx
|
||||
:remote-tx remote-tx
|
||||
:local-checksum local-checksum
|
||||
:remote-checksum remote-checksum})]
|
||||
(sync-log-state/rtc-log :rtc.log/checksum-mismatch mismatch-data)
|
||||
(log/warn :db-sync/checksum-mismatch mismatch-data)))))))
|
||||
|
||||
(defn- handle-tx-reject!
|
||||
[repo client message local-tx]
|
||||
(let [reason (:reason message)
|
||||
remote-tx (:t message)]
|
||||
remote-tx (:t message)
|
||||
success-tx-ids (:success-tx-ids message)
|
||||
failed-tx-id (:failed-tx-id message)]
|
||||
(when (nil? reason)
|
||||
(fail-fast :db-sync/missing-field
|
||||
{:repo repo :type "tx/reject" :field :reason}))
|
||||
(when (contains? message :t)
|
||||
(require-non-negative remote-tx {:repo repo :type "tx/reject"}))
|
||||
(when (contains? message :success-tx-ids)
|
||||
(require-seq success-tx-ids {:repo repo :type "tx/reject" :field :success-tx-ids})
|
||||
(doseq [tx-id success-tx-ids]
|
||||
(require-uuid tx-id {:repo repo :type "tx/reject" :field :success-tx-ids})))
|
||||
(when (contains? message :failed-tx-id)
|
||||
(require-uuid failed-tx-id {:repo repo :type "tx/reject" :field :failed-tx-id}))
|
||||
(case reason
|
||||
"stale"
|
||||
(when (and (:ws client) (ws-open? (:ws client)))
|
||||
(send! (:ws client) {:type "pull" :since local-tx}))
|
||||
(request-pull! client local-tx)
|
||||
|
||||
(let [data (when-let [raw-data (:data message)]
|
||||
(let [inflight @(:inflight client)
|
||||
inflight-set (set inflight)
|
||||
successful-tx-ids (->> (or success-tx-ids [])
|
||||
(filter inflight-set)
|
||||
vec)
|
||||
failed-tx-id (when (and failed-tx-id (contains? inflight-set failed-tx-id))
|
||||
failed-tx-id)
|
||||
data (when-let [raw-data (:data message)]
|
||||
(parse-transit raw-data
|
||||
{:repo repo
|
||||
:type "tx/reject"
|
||||
:reason reason
|
||||
:field :data}))]
|
||||
:field :data}))
|
||||
rejected-data (cond-> {:type :db-sync/tx-rejected
|
||||
:repo repo
|
||||
:message-type "tx/reject"
|
||||
:reason reason}
|
||||
(contains? message :t) (assoc :t remote-tx)
|
||||
(seq successful-tx-ids) (assoc :success-tx-ids successful-tx-ids)
|
||||
(some? failed-tx-id) (assoc :failed-tx-id failed-tx-id)
|
||||
(some? data) (assoc :data data))]
|
||||
(if (or (contains? message :success-tx-ids)
|
||||
(contains? message :failed-tx-id))
|
||||
(do
|
||||
(sync-apply/mark-pending-txs-false! repo successful-tx-ids)
|
||||
(when failed-tx-id
|
||||
(sync-apply/mark-failed-txs! repo [failed-tx-id])))
|
||||
;; Backward compatibility for older servers without per-tx reject metadata.
|
||||
(sync-apply/mark-failed-txs! repo inflight))
|
||||
(reset! (:inflight client) [])
|
||||
(broadcast-rtc-state! client)
|
||||
(sync-log-state/rtc-log :rtc.log/tx-rejected rejected-data)
|
||||
(fail-fast :db-sync/tx-rejected
|
||||
(cond-> {:type :db-sync/tx-rejected
|
||||
:repo repo
|
||||
:message-type "tx/reject"
|
||||
:reason reason}
|
||||
(contains? message :t) (assoc :t remote-tx)
|
||||
(some? data) (assoc :data data)))))))
|
||||
rejected-data)))))
|
||||
|
||||
(defn- handle-hello!
|
||||
[repo client local-tx remote-tx remote-checksum]
|
||||
@@ -173,14 +231,14 @@
|
||||
(verify-sync-checksum! repo client local-tx remote-tx remote-checksum {:type "hello"})
|
||||
(broadcast-rtc-state! client)
|
||||
(when (> remote-tx local-tx)
|
||||
(send! (:ws client) {:type "pull" :since local-tx}))
|
||||
(request-pull! client local-tx))
|
||||
(sync-assets/enqueue-asset-sync!
|
||||
repo client
|
||||
{:enqueue-asset-task-f enqueue-asset-task!
|
||||
:current-client-f current-client
|
||||
:broadcast-rtc-state!-f broadcast-rtc-state!
|
||||
:fail-fast-f fail-fast})
|
||||
(sync-apply/flush-pending! repo client))
|
||||
(sync-apply/enqueue-flush-pending! repo client))
|
||||
|
||||
(defn- handle-online-users!
|
||||
[repo client message]
|
||||
@@ -199,16 +257,64 @@
|
||||
(defn- handle-tx-batch-ok!
|
||||
[repo client remote-tx remote-checksum]
|
||||
(require-non-negative remote-tx {:repo repo :type "tx/batch/ok"})
|
||||
(client-op/update-local-tx repo remote-tx)
|
||||
(sync-util/clear-last-sync-error! client)
|
||||
(broadcast-rtc-state! client)
|
||||
(sync-apply/remove-pending-txs! repo @(:inflight client))
|
||||
(reset! (:inflight client) [])
|
||||
(verify-sync-checksum! repo client remote-tx remote-tx remote-checksum {:type "tx/batch/ok"})
|
||||
(sync-apply/flush-pending! repo client))
|
||||
(let [current-local-tx (or (client-op/get-local-tx repo) 0)
|
||||
next-local-tx (max current-local-tx remote-tx)]
|
||||
(client-op/update-local-tx repo next-local-tx)
|
||||
(sync-util/clear-last-sync-error! client)
|
||||
(broadcast-rtc-state! client)
|
||||
(sync-apply/mark-pending-txs-false! repo @(:inflight client))
|
||||
(reset! (:inflight client) [])
|
||||
(verify-sync-checksum! repo client next-local-tx remote-tx remote-checksum {:type "tx/batch/ok"})
|
||||
(sync-apply/enqueue-flush-pending! repo client)))
|
||||
|
||||
(defn- update-latest-remote-state!
|
||||
[repo message]
|
||||
(let [remote-tx (:t message)
|
||||
remote-checksum (:checksum message)
|
||||
has-checksum? (contains? message :checksum)
|
||||
latest-remote-tx (get @sync-apply/*repo->latest-remote-tx repo)
|
||||
stale-remote-tx? (and (number? remote-tx)
|
||||
(number? latest-remote-tx)
|
||||
(< remote-tx latest-remote-tx))]
|
||||
(when (number? remote-tx)
|
||||
(swap! sync-apply/*repo->latest-remote-tx
|
||||
update repo
|
||||
(fn [prev]
|
||||
(if (number? prev)
|
||||
(max prev remote-tx)
|
||||
remote-tx))))
|
||||
(when (and has-checksum? (not stale-remote-tx?))
|
||||
(swap! sync-apply/*repo->latest-remote-checksum assoc repo remote-checksum))
|
||||
{:stale-remote-tx? stale-remote-tx?
|
||||
:latest-remote-tx-before latest-remote-tx}))
|
||||
|
||||
(declare handle-pull-ok! handle-changed!)
|
||||
|
||||
(defn handle-message!
|
||||
[repo client raw]
|
||||
(let [message (-> raw
|
||||
sync-transport/parse-message
|
||||
sync-transport/coerce-ws-server-message)]
|
||||
(when-not (map? message)
|
||||
(fail-fast :db-sync/response-parse-failed {:repo repo :raw raw}))
|
||||
(let [local-tx (or (client-op/get-local-tx repo) 0)
|
||||
remote-tx (:t message)
|
||||
remote-checksum (:checksum message)]
|
||||
(update-latest-remote-state! repo message)
|
||||
(case (:type message)
|
||||
"hello" (handle-hello! repo client local-tx remote-tx remote-checksum)
|
||||
"online-users" (handle-online-users! repo client message)
|
||||
"presence" (handle-presence! client message)
|
||||
"tx/batch/ok" (handle-tx-batch-ok! repo client remote-tx remote-checksum)
|
||||
"pull/ok" (handle-pull-ok! repo client local-tx remote-tx remote-checksum message)
|
||||
"changed" (handle-changed! repo client local-tx remote-tx)
|
||||
"tx/reject" (handle-tx-reject! repo client message local-tx)
|
||||
(fail-fast :db-sync/invalid-field
|
||||
{:repo repo :type (:type message)})))))
|
||||
|
||||
(defn- handle-pull-ok!
|
||||
[repo client local-tx remote-tx remote-checksum message]
|
||||
(clear-pending-pull! client)
|
||||
(when (> remote-tx local-tx)
|
||||
(let [txs (:txs message)]
|
||||
(require-non-negative remote-tx {:repo repo :type "pull/ok"})
|
||||
@@ -239,7 +345,7 @@
|
||||
(client-op/update-local-tx repo remote-tx)
|
||||
(broadcast-rtc-state! client)
|
||||
(verify-sync-checksum! repo client remote-tx remote-tx remote-checksum {:type "pull/ok"})
|
||||
(sync-apply/flush-pending! repo client))
|
||||
(sync-apply/enqueue-flush-pending! repo client))
|
||||
(p/then (fn [_]
|
||||
(sync-util/clear-last-sync-error! client)))
|
||||
(p/catch (fn [error]
|
||||
@@ -250,29 +356,4 @@
|
||||
(require-non-negative remote-tx {:repo repo :type "changed"})
|
||||
(broadcast-rtc-state! client)
|
||||
(when (< local-tx remote-tx)
|
||||
(send! (:ws client) {:type "pull" :since local-tx})))
|
||||
|
||||
(defn handle-message!
|
||||
[repo client raw]
|
||||
(let [message (-> raw
|
||||
sync-transport/parse-message
|
||||
sync-transport/coerce-ws-server-message)]
|
||||
(when-not (map? message)
|
||||
(fail-fast :db-sync/response-parse-failed {:repo repo :raw raw}))
|
||||
(let [local-tx (or (client-op/get-local-tx repo) 0)
|
||||
remote-tx (:t message)
|
||||
remote-checksum (:checksum message)]
|
||||
(when remote-tx
|
||||
(swap! sync-apply/*repo->latest-remote-tx assoc repo remote-tx))
|
||||
(when (contains? message :checksum)
|
||||
(swap! sync-apply/*repo->latest-remote-checksum assoc repo remote-checksum))
|
||||
(case (:type message)
|
||||
"hello" (handle-hello! repo client local-tx remote-tx remote-checksum)
|
||||
"online-users" (handle-online-users! repo client message)
|
||||
"presence" (handle-presence! client message)
|
||||
"tx/batch/ok" (handle-tx-batch-ok! repo client remote-tx remote-checksum)
|
||||
"pull/ok" (handle-pull-ok! repo client local-tx remote-tx remote-checksum message)
|
||||
"changed" (handle-changed! repo client local-tx remote-tx)
|
||||
"tx/reject" (handle-tx-reject! repo client message local-tx)
|
||||
(fail-fast :db-sync/invalid-field
|
||||
{:repo repo :type (:type message)})))))
|
||||
(request-pull! client local-tx)))
|
||||
|
||||
@@ -15,6 +15,8 @@
|
||||
(defkeywords
|
||||
:rtc.log/upload {:doc "rtc log type for upload-graph."}
|
||||
:rtc.log/download {:doc "rtc log type for upload-graph."}
|
||||
:rtc.log/checksum-mismatch {:doc "local/remote checksum mismatch detected"}
|
||||
:rtc.log/tx-rejected {:doc "tx/reject received from server"}
|
||||
:rtc.asset.log/upload-assets {:doc "upload local assets to remote"}
|
||||
:rtc.asset.log/download-assets {:doc "download assets from remote"}
|
||||
:rtc.asset.log/remove-assets {:doc "remove remote assets"}
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
(ns frontend.worker.sync.presence
|
||||
"Presence and rtc state helpers for db sync."
|
||||
(:require [datascript.core :as d]
|
||||
[logseq.common.util :as common-util]
|
||||
(:require [logseq.common.util :as common-util]
|
||||
[frontend.worker.state :as worker-state]))
|
||||
|
||||
(defn current-client
|
||||
@@ -16,7 +15,6 @@
|
||||
|
||||
(defn sync-counts
|
||||
[{:keys [get-datascript-conn
|
||||
get-client-ops-conn
|
||||
get-pending-local-tx-count
|
||||
get-unpushed-asset-ops-count
|
||||
get-local-tx
|
||||
@@ -28,8 +26,7 @@
|
||||
(when (get-datascript-conn repo)
|
||||
(let [pending-local (if get-pending-local-tx-count
|
||||
(get-pending-local-tx-count repo)
|
||||
(when-let [conn (client-ops-conn get-client-ops-conn repo)]
|
||||
(count (d/datoms @conn :avet :db-sync/pending? true))))
|
||||
0)
|
||||
pending-asset (get-unpushed-asset-ops-count repo)
|
||||
local-tx (get-local-tx repo)
|
||||
remote-tx (get latest-remote-tx repo)
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
first)]
|
||||
(let [[content addresses] (bean/->clj result)
|
||||
addresses (when addresses (js/JSON.parse addresses))
|
||||
data (sqlite-util/transit-read content)]
|
||||
data (sqlite-util/read-transit-str content)]
|
||||
(if (and addresses (map? data))
|
||||
(assoc data :addresses addresses)
|
||||
data))))
|
||||
@@ -48,7 +48,7 @@
|
||||
(when-let [addresses (:addresses data)]
|
||||
(js/JSON.stringify (bean/->js addresses))))]
|
||||
#js {:$addr addr
|
||||
:$content (sqlite-util/transit-write data')
|
||||
:$content (sqlite-util/write-transit-str data')
|
||||
:$addresses addresses}))
|
||||
addr+data-seq)]
|
||||
(upsert-addr-content! db data)))
|
||||
|
||||
@@ -53,9 +53,23 @@
|
||||
(defn coerce-ws-server-message
|
||||
[message]
|
||||
(when message
|
||||
(let [coerced (coerce db-sync-schema/ws-server-message-coercer message {:schema :ws/server})]
|
||||
(when-not (= coerced invalid-coerce)
|
||||
coerced))))
|
||||
(letfn [(uuid-like->string [value]
|
||||
(cond
|
||||
(uuid? value) (str value)
|
||||
(and (map? value) (string? (:uuid value))) (:uuid value)
|
||||
:else value))
|
||||
(normalize-legacy-tx-reject [m]
|
||||
(if (= "tx/reject" (:type m))
|
||||
(cond-> m
|
||||
(contains? m :failed-tx-id) (update :failed-tx-id uuid-like->string)
|
||||
(contains? m :success-tx-ids) (update :success-tx-ids
|
||||
(fn [ids]
|
||||
(mapv uuid-like->string (or ids [])))))
|
||||
m))]
|
||||
(let [message* (normalize-legacy-tx-reject message)
|
||||
coerced (coerce db-sync-schema/ws-server-message-coercer message* {:schema :ws/server})]
|
||||
(when-not (= coerced invalid-coerce)
|
||||
coerced)))))
|
||||
|
||||
(defn parse-transit
|
||||
[fail-fast-f value context]
|
||||
@@ -82,5 +96,14 @@
|
||||
[coerce-ws-client-message-f ws message]
|
||||
(when (ws-open? ws)
|
||||
(if-let [coerced (coerce-ws-client-message-f message)]
|
||||
(.send ws (js/JSON.stringify (clj->js coerced)))
|
||||
(let [message* (if (= "tx/batch" (:type coerced))
|
||||
(update coerced :txs
|
||||
(fn [txs]
|
||||
(mapv (fn [tx-entry]
|
||||
(if-let [tx-id (:tx-id tx-entry)]
|
||||
(assoc tx-entry :tx-id (str tx-id))
|
||||
tx-entry))
|
||||
txs)))
|
||||
coerced)]
|
||||
(.send ws (js/JSON.stringify (clj->js message*))))
|
||||
(log/error :db-sync/ws-request-invalid {:message message}))))
|
||||
|
||||
@@ -100,7 +100,7 @@
|
||||
:process-batch-f
|
||||
(fn [batch]
|
||||
(p/let [datoms* (sync-large-title/offload-large-titles-in-datoms-batch
|
||||
repo graph-id batch aes-key sync-large-title/upload-large-title!)
|
||||
repo graph-id batch aes-key sync-apply/upload-large-title!)
|
||||
encrypted-datoms (if aes-key
|
||||
(sync-crypt/<encrypt-datoms aes-key datoms*)
|
||||
datoms*)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
"Undo redo new implementation"
|
||||
(:require [datascript.core :as d]
|
||||
[frontend.worker.state :as worker-state]
|
||||
[frontend.worker.sync.client-op :as client-op]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.common.defkeywords :refer [defkeywords]]
|
||||
[malli.core :as m]
|
||||
@@ -45,8 +46,10 @@
|
||||
[:added-ids [:set :int]]
|
||||
[:retracted-ids [:set :int]]
|
||||
[:db-sync/tx-id {:optional true} :uuid]
|
||||
[:db-sync/forward-outliner-ops {:optional true} [:sequential :any]]
|
||||
[:db-sync/inverse-outliner-ops {:optional true} [:sequential :any]]]]]
|
||||
[:db-sync/forward-outliner-ops {:optional true}
|
||||
[:maybe [:sequential :any]]]
|
||||
[:db-sync/inverse-outliner-ops {:optional true}
|
||||
[:maybe [:sequential :any]]]]]]
|
||||
|
||||
[::record-editor-info
|
||||
[:cat :keyword
|
||||
@@ -206,7 +209,7 @@
|
||||
[repo undo?]
|
||||
(undo-redo-aux repo undo?))
|
||||
|
||||
(defn- run-worker-path
|
||||
(defn- apply-history-action
|
||||
[repo conn undo? op tx-meta' tx-id]
|
||||
(if-let [apply-action @*apply-history-action!]
|
||||
(try
|
||||
@@ -253,14 +256,10 @@
|
||||
(second %))
|
||||
op)]
|
||||
(let [tx-id (:db-sync/tx-id data)
|
||||
forward-outliner-ops (:db-sync/forward-outliner-ops data)
|
||||
inverse-outliner-ops (:db-sync/inverse-outliner-ops data)
|
||||
tx-meta' (-> (undo-redo-action-meta data undo?)
|
||||
(assoc :forward-outliner-ops forward-outliner-ops
|
||||
:inverse-outliner-ops inverse-outliner-ops
|
||||
:db-sync/forward-outliner-ops forward-outliner-ops
|
||||
:db-sync/inverse-outliner-ops inverse-outliner-ops))]
|
||||
(run-worker-path repo conn undo? op tx-meta' tx-id))))
|
||||
tx-meta' (merge (undo-redo-action-meta data undo?)
|
||||
(select-keys data [:db-sync/forward-outliner-ops
|
||||
:db-sync/inverse-outliner-ops]))]
|
||||
(apply-history-action repo conn undo? op tx-meta' tx-id))))
|
||||
|
||||
(defn- undo-redo-aux
|
||||
[repo undo?]
|
||||
@@ -302,10 +301,7 @@
|
||||
(defn- pending-history-action-ops
|
||||
[repo tx-id]
|
||||
(when (uuid? tx-id)
|
||||
(when-let [conn (get @worker-state/*client-ops-conns repo)]
|
||||
(when-let [ent (d/entity @conn [:db-sync/tx-id tx-id])]
|
||||
{:db-sync/forward-outliner-ops (some-> (:db-sync/forward-outliner-ops ent) seq vec)
|
||||
:db-sync/inverse-outliner-ops (some-> (:db-sync/inverse-outliner-ops ent) seq vec)}))))
|
||||
(client-op/history-action-ops-by-tx-id repo tx-id)))
|
||||
|
||||
(defn gen-undo-ops!
|
||||
[repo {:keys [tx-data tx-meta db-after db-before]} tx-id
|
||||
@@ -319,8 +315,7 @@
|
||||
outliner-op
|
||||
(not (false? (:gen-undo-ops? tx-meta)))
|
||||
(not (:create-today-journal? tx-meta))
|
||||
(seq forward-outliner-ops)
|
||||
(seq inverse-outliner-ops))
|
||||
(not (contains? #{:create-view} (:source-outliner-op tx-meta))))
|
||||
(let [all-ids (distinct (map :e tx-data))
|
||||
retracted-ids (set
|
||||
(filter
|
||||
|
||||
@@ -23,7 +23,11 @@
|
||||
(do ~@body))))
|
||||
|
||||
#?(:cljs
|
||||
(def dev? js/goog.DEBUG))
|
||||
(do
|
||||
(goog-define NODETEST false)
|
||||
(def dev? js/goog.DEBUG)
|
||||
(def node-test? NODETEST)
|
||||
(def dev-or-test? (or dev? node-test?))))
|
||||
|
||||
#?(:cljs
|
||||
(do
|
||||
|
||||
@@ -83,6 +83,7 @@
|
||||
(def ^:export clear_right_sidebar_blocks api-app/clear_right_sidebar_blocks)
|
||||
(def ^:export push_state api-app/push_state)
|
||||
(def ^:export replace_state api-app/replace_state)
|
||||
(def ^:export get_current_route api-app/get_current_route)
|
||||
|
||||
;; db
|
||||
(def ^:export q api-db/q)
|
||||
@@ -181,7 +182,9 @@
|
||||
;; search
|
||||
(defn ^:export search
|
||||
[q' & [opts]]
|
||||
(-> (search-handler/search (state/get-current-repo) q' (if opts (js->clj opts :keywordize-keys true) {}))
|
||||
(-> (search-handler/search
|
||||
(state/get-current-repo) q'
|
||||
(if opts (js->clj opts :keywordize-keys true) {}))
|
||||
(p/then #(bean/->js (sdk-utils/normalize-keyword-for-json %)))))
|
||||
|
||||
;; helpers
|
||||
|
||||
@@ -172,3 +172,9 @@
|
||||
(if-let [page-name (and page? (:name params))]
|
||||
(route-handler/redirect-to-page! page-name {:anchor (:anchor query) :push false})
|
||||
(rfe/replace-state k params query)))))
|
||||
|
||||
(def get_current_route
|
||||
(fn []
|
||||
(some-> (state/get-route-match)
|
||||
(dissoc :data)
|
||||
(bean/->js))))
|
||||
|
||||
@@ -295,17 +295,21 @@
|
||||
(resolve-eid this prop-uuid-or-ident-or-title
|
||||
api-block/resolve-property-prefix-for-db))
|
||||
|
||||
(defn- get-tags [name]
|
||||
(some->> (entity-util/get-pages-by-name (db-conn/get-db) name)
|
||||
(map #(some-> % (first) (db/entity)))
|
||||
(filter ldb/class?)))
|
||||
|
||||
(defn get-tag [class-uuid-or-ident-or-title]
|
||||
(this-as this
|
||||
(let [eid (resolve-tag-eid this class-uuid-or-ident-or-title)
|
||||
tag (db/entity eid)]
|
||||
tag (db/entity eid)
|
||||
tag (or tag (some-> (get-tags class-uuid-or-ident-or-title) first))]
|
||||
(when (ldb/class? tag)
|
||||
(sdk-utils/result->js tag)))))
|
||||
|
||||
(defn get-tags-by-name [name]
|
||||
(when-let [tags (some->> (entity-util/get-pages-by-name (db-conn/get-db) name)
|
||||
(map #(some-> % (first) (db/entity)))
|
||||
(filter ldb/class?))]
|
||||
(when-let [tags (get-tags name)]
|
||||
(sdk-utils/result->js tags)))
|
||||
|
||||
(defn tag-add-property [tag-id property-id-or-name]
|
||||
|
||||
@@ -56,7 +56,7 @@
|
||||
(defn import-edn
|
||||
"Given EDN data as a transitized string, converts to EDN and imports it."
|
||||
[edn-data*]
|
||||
(p/let [edn-data (sqlite-util/transit-read edn-data*)
|
||||
(p/let [edn-data (sqlite-util/read-transit-str edn-data*)
|
||||
{:keys [error]} (ui-outliner-tx/transact!
|
||||
{:outliner-op :batch-import-edn}
|
||||
(outliner-op/batch-import-edn! edn-data {}))]
|
||||
@@ -72,5 +72,5 @@
|
||||
result (state/<invoke-db-worker :thread-api/export-edn (state/get-current-repo) options)]
|
||||
(when (:export-edn-error result)
|
||||
(throw (ex-info (str "Export EDN Error: " (:export-edn-error result)) {})))
|
||||
{:export-body (sqlite-util/transit-write result)
|
||||
:graph (string/replace-first (state/get-current-repo) common-config/db-version-prefix "")}))
|
||||
{:export-body (sqlite-util/write-transit-str result)
|
||||
:graph (string/replace-first (state/get-current-repo) common-config/db-version-prefix "")}))
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
[logseq.api.block :as api-block]
|
||||
[logseq.api.db-based :as db-based-api]
|
||||
[logseq.common.path :as path]
|
||||
[logseq.outliner.property :as outliner-property]
|
||||
[logseq.common.util.date-time :as date-time-util]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.sdk.core]
|
||||
@@ -439,35 +440,56 @@
|
||||
key (api-block/get-db-ident-from-property-name key this)]
|
||||
(property-handler/remove-block-property! block-uuid key))))))
|
||||
|
||||
(defn- get-block-classes-properties-has-default-value
|
||||
[block-id]
|
||||
(when-let [classes-properties
|
||||
(some-> (outliner-property/get-block-classes-properties (db/get-db) block-id)
|
||||
:classes-properties)]
|
||||
(let [properties (->> classes-properties
|
||||
(filter :logseq.property/default-value)
|
||||
(map (fn [property]
|
||||
[(:db/ident property)
|
||||
(:logseq.property/default-value property)])))
|
||||
properties (into {} properties)]
|
||||
properties)))
|
||||
|
||||
(defn- get-all-block-properties
|
||||
[id]
|
||||
(p/let [block (<get-block id {:children? false})]
|
||||
(when-let [own-properties (:block/properties block)]
|
||||
(let [classes-properties (get-block-classes-properties-has-default-value (:db/id block))
|
||||
properties (if (seq classes-properties)
|
||||
(merge classes-properties own-properties)
|
||||
own-properties)]
|
||||
properties))))
|
||||
|
||||
(defn get_block_property
|
||||
[id key]
|
||||
(this-as this
|
||||
(p/let [block (<get-block id {:children? false})]
|
||||
(when-let [properties (some-> block (:block/uuid) (db-model/get-block-by-uuid) (:block/properties))]
|
||||
(when (seq properties)
|
||||
(let [property-name (api-block/sanitize-user-property-name key)
|
||||
ident (api-block/get-db-ident-from-property-name property-name this)
|
||||
property-value (or (get properties property-name)
|
||||
(get properties (keyword property-name))
|
||||
(get properties ident))
|
||||
property-value (if-let [property-id (:db/id property-value)]
|
||||
(db/pull property-id) property-value)
|
||||
property-value (cond-> property-value
|
||||
(map? property-value)
|
||||
(assoc
|
||||
:value (or (:logseq.property/value property-value)
|
||||
(:block/title property-value))
|
||||
:ident ident))
|
||||
parsed-value (api-block/parse-property-json-value-if-need ident property-value)]
|
||||
(or parsed-value
|
||||
(bean/->js (sdk-utils/normalize-keyword-for-json property-value)))))))))
|
||||
(p/let [properties (get-all-block-properties id)]
|
||||
(when (seq properties)
|
||||
(let [property-name (api-block/sanitize-user-property-name key)
|
||||
ident (api-block/get-db-ident-from-property-name property-name this)
|
||||
property-value (or (get properties property-name)
|
||||
(get properties (keyword property-name))
|
||||
(get properties ident))
|
||||
property-value (if-let [property-id (:db/id property-value)]
|
||||
(db/pull property-id) property-value)
|
||||
property-value (cond-> property-value
|
||||
(map? property-value)
|
||||
(assoc
|
||||
:value (or (:logseq.property/value property-value)
|
||||
(:block/title property-value))
|
||||
:ident ident))
|
||||
parsed-value (api-block/parse-property-json-value-if-need ident property-value)]
|
||||
(or parsed-value
|
||||
(sdk-utils/result->js property-value)))))))
|
||||
|
||||
(def get_block_properties
|
||||
(fn [id]
|
||||
(p/let [block (<get-block id {:children? false})]
|
||||
(when block
|
||||
(let [properties (api-block/into-readable-db-properties (:block/properties block))]
|
||||
(sdk-utils/result->js properties))))))
|
||||
(p/let [properties (get-all-block-properties id)]
|
||||
(when-let [properties (some-> properties (api-block/into-readable-db-properties))]
|
||||
(sdk-utils/result->js properties)))))
|
||||
|
||||
(defn get_page_properties
|
||||
[id-or-page-name]
|
||||
|
||||
@@ -8,3 +8,26 @@
|
||||
(get @state/state (keyword path))
|
||||
@state/state)
|
||||
(bean/->js)))
|
||||
|
||||
(defn- current-repo-or-throw
|
||||
[]
|
||||
(if-let [repo (state/get-current-repo)]
|
||||
repo
|
||||
(throw (ex-info "No current repo" {:type :debug/no-current-repo}))))
|
||||
|
||||
(defn ^:export syncStopUpload
|
||||
[]
|
||||
(state/<invoke-db-worker :thread-api/db-sync-stop-upload (current-repo-or-throw)))
|
||||
|
||||
(defn ^:export syncResumeUpload
|
||||
[]
|
||||
(state/<invoke-db-worker :thread-api/db-sync-resume-upload (current-repo-or-throw)))
|
||||
|
||||
(defn ^:export syncUploadStopped
|
||||
[]
|
||||
(state/<invoke-db-worker :thread-api/db-sync-upload-stopped? (current-repo-or-throw)))
|
||||
|
||||
;; Snake_case aliases for consistency with existing debug exports.
|
||||
(defn ^:export sync_stop_upload [] (syncStopUpload))
|
||||
(defn ^:export sync_resume_upload [] (syncResumeUpload))
|
||||
(defn ^:export sync_upload_stopped [] (syncUploadStopped))
|
||||
|
||||
@@ -39,6 +39,13 @@
|
||||
(keyword pid) key (reduce #(assoc %1 %2 (aget opts (name %2))) {}
|
||||
[:before :subs :render]))))
|
||||
|
||||
(defn ^:export register_hosted_renderer
|
||||
[pid key ^js opts]
|
||||
(when-let [^js _pl (plugin-handler/get-plugin-inst pid)]
|
||||
(plugin-handler/register-hosted-renderer
|
||||
(keyword pid) key (reduce #(assoc %1 %2 (aget opts (name %2))) {}
|
||||
[:title :type :mode :subs :render]))))
|
||||
|
||||
(defn ^:export register_extensions_enhancer
|
||||
[pid type enhancer]
|
||||
(when-let [^js _pl (and (fn? enhancer) (plugin-handler/get-plugin-inst pid))]
|
||||
|
||||
@@ -142,6 +142,14 @@
|
||||
(shui/popup-show! nil (fn [] (log)) {}))}
|
||||
[:span.text-base "Check log"]]
|
||||
|
||||
[:div.mobile-setting-item
|
||||
{:on-click #(state/pub-event! [:go/sync-server-settings])}
|
||||
[:span.text-base "Sync server URL"]
|
||||
[:span.text-sm.opacity-70
|
||||
(if-let [custom (config/get-custom-sync-server-url)]
|
||||
custom
|
||||
"Logseq sync (default)")]]
|
||||
|
||||
(when login?
|
||||
[:div.mobile-setting-item
|
||||
{:on-click (fn []
|
||||
|
||||
@@ -102,7 +102,6 @@
|
||||
:page/logseq-is-having-a-problem "Logseq يواجه مشكلة. لمحاولة إعادته إلى حالة العمل، يرجى اتباع الخطوات الآمنة التالية بالترتيب:"
|
||||
:page/step "الخطوة {1}"
|
||||
:page/try "حاول"
|
||||
:page/delete-confirmation "هل أنت متأكد من رغبتك في حذف هذه الصفحة؟"
|
||||
:page/db-delete-confirmation "هل أنت متأكد من رغبتك في حذف هذه الصفحة؟"
|
||||
:page/make-public "اجعلها متاحة للنشر"
|
||||
:page/make-private "اجعله خاصاً"
|
||||
|
||||
@@ -196,7 +196,6 @@
|
||||
:page/created-at "Erstellt am"
|
||||
:page/db-delete-confirmation "Diese Seite wirklich entfernen?"
|
||||
:page/delete "Seite löschen"
|
||||
:page/delete-confirmation "Diese Seite wirklich entfernen?"
|
||||
:page/logseq-is-having-a-problem "Logseq hat ein Problem festgestellt. Versuche zurückzukehren ..."
|
||||
:page/make-private "Privat machen"
|
||||
:page/make-public "Beim Export in HTML veröffentlichen"
|
||||
|
||||
@@ -102,8 +102,9 @@
|
||||
:page/logseq-is-having-a-problem "Logseq is having a problem. To try to get it back to a working state, please try the following safe steps in order:"
|
||||
:page/step "Step {1}"
|
||||
:page/try "Try"
|
||||
:page/delete-confirmation "Are you sure you want to delete this page?"
|
||||
:page/batch-delete-confirmation "Are you sure you want to delete these page(s)? Properties and tags will be permanently deleted and pages will be recycled."
|
||||
:page/db-delete-confirmation "Are you sure you want to delete this page?"
|
||||
:page/permanently-delete-confirmation "Are you sure you want to permanently delete this page?"
|
||||
:page/make-public "Make it public for publishing"
|
||||
:page/make-private "Make it private"
|
||||
:page/delete "Delete page"
|
||||
@@ -127,6 +128,7 @@
|
||||
:linked-references/filter-excludes "Excludes: "
|
||||
:editor/block-search "Search for a block"
|
||||
:text/image "Image"
|
||||
:asset/show-file-in-folder "Show file in folder"
|
||||
:asset/show-in-folder "Show image in folder"
|
||||
:asset/open-in-browser "Open image in browser"
|
||||
:asset/delete "Delete image"
|
||||
@@ -222,6 +224,12 @@
|
||||
:settings-page/plugin-system "Plugins"
|
||||
:settings-page/enable-flashcards "Flashcards"
|
||||
:settings-page/network-proxy "Network proxy"
|
||||
:settings-page/sync-server-url "Sync server URL"
|
||||
:settings-page/sync-server-url-desc "Set a custom HTTPS sync server URL for self-hosted sync. Your Logseq authentication tokens will be sent to this server, so only use a trusted URL. Leave empty to use the official Logseq sync."
|
||||
:settings-page/sync-server-url-saved "Sync server URL saved."
|
||||
:settings-page/sync-server-url-cleared "Sync server URL cleared. Using official Logseq sync."
|
||||
:settings-page/sync-server-url-default "Logseq sync (default)"
|
||||
:settings-page/sync-server-url-reset "Reset to default"
|
||||
:settings-page/login-prompt "To access new features before anyone else you must be an Open Collective Sponsor or Backer of Logseq and therefore log in first."
|
||||
:settings-page/native-titlebar "Native title bar"
|
||||
:settings-page/native-titlebar-desc "Enables the native window title bar on Windows and Linux."
|
||||
@@ -581,6 +589,7 @@
|
||||
:dev/replace-graph-with-db-file "(Dev) Replace graph with its db.sqlite file"
|
||||
:dev/validate-db "(Dev) Validate current graph"
|
||||
:dev/recompute-checksum "(Dev) Recompute graph checksum"
|
||||
:dev/export-client-ops-sqlite "(Dev) Export client ops sqlite"
|
||||
:dev/gc-graph "(Dev) Garbage collect graph (remove unused data in SQLite)"
|
||||
:dev/rtc-stop "(Dev) RTC Stop"
|
||||
:dev/rtc-start "(Dev) RTC Start"
|
||||
|
||||
@@ -101,7 +101,6 @@
|
||||
:page/logseq-is-having-a-problem "Logseq está com um problema. Para tentar fazê-lo voltar a funcionar, siga as etapas seguras a seguir:"
|
||||
:page/step "Etapa {1}"
|
||||
:page/try "Tentar"
|
||||
:page/delete-confirmation "Você tem certeza que quer deletar essa página?"
|
||||
:page/db-delete-confirmation "Você tem certeza que quer deletar essa página?"
|
||||
:page/make-public "Torná-la pública para publicação"
|
||||
:page/make-private "Tornar privado"
|
||||
|
||||
@@ -264,7 +264,7 @@
|
||||
:color/red "红色"
|
||||
:color/yellow "黄色"
|
||||
|
||||
:updater/new-version-install "新版本已经准备就绪,重启应用即可更新。"
|
||||
:updater/new-version-install "新版本已经准备就绪。"
|
||||
:updater/quit-and-install "现在安装"
|
||||
|
||||
:notification/clear-all "清除全部通知"
|
||||
|
||||
35
src/test/frontend/components/property/property_test.cljs
Normal file
35
src/test/frontend/components/property/property_test.cljs
Normal file
@@ -0,0 +1,35 @@
|
||||
(ns frontend.components.property.property-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
[frontend.components.property :as property-component]))
|
||||
|
||||
(deftest sanitize-property-values-for-display-filters-recycled-entity-values-test
|
||||
(let [active-value {:db/id 101
|
||||
:block/title "Active"}
|
||||
recycled-value {:db/id 102
|
||||
:block/title "Recycled"
|
||||
:logseq.property/deleted-at 1}
|
||||
{:keys [properties recycled-only-property-ids]}
|
||||
(#'property-component/sanitize-property-values-for-display
|
||||
{:user.property/node #{active-value recycled-value}
|
||||
:user.property/single recycled-value
|
||||
:user.property/scalar "ok"})]
|
||||
(is (= #{active-value}
|
||||
(:user.property/node properties)))
|
||||
(is (nil? (:user.property/single properties)))
|
||||
(is (= "ok" (:user.property/scalar properties)))
|
||||
(is (= #{:user.property/single}
|
||||
recycled-only-property-ids))))
|
||||
|
||||
(deftest sanitize-property-values-for-display-marks-all-recycled-coll-as-hidden-test
|
||||
(let [recycled-a {:db/id 201
|
||||
:block/title "Recycled A"
|
||||
:logseq.property/deleted-at 1}
|
||||
recycled-b {:db/id 202
|
||||
:block/title "Recycled B"
|
||||
:logseq.property/deleted-at 2}
|
||||
{:keys [properties recycled-only-property-ids]}
|
||||
(#'property-component/sanitize-property-values-for-display
|
||||
{:user.property/nodes [recycled-a recycled-b]})]
|
||||
(is (nil? (:user.property/nodes properties)))
|
||||
(is (= #{:user.property/nodes}
|
||||
recycled-only-property-ids))))
|
||||
67
src/test/frontend/components/property/value_test.cljs
Normal file
67
src/test/frontend/components/property/value_test.cljs
Normal file
@@ -0,0 +1,67 @@
|
||||
(ns frontend.components.property.value-test
|
||||
(:require [cljs.test :refer [async deftest is]]
|
||||
[frontend.components.property.value :as property-value]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(deftest resolve-journal-page-for-date-returns-existing-page-test
|
||||
(async done
|
||||
(let [existing-page {:db/id 100
|
||||
:block/journal-day 20250102}
|
||||
created?* (atom false)]
|
||||
(-> (#'property-value/<resolve-journal-page-for-date
|
||||
(js/Date. "2025-01-02T00:00:00Z")
|
||||
(constantly "test-repo")
|
||||
(fn [_repo _title _opts]
|
||||
(p/resolved existing-page))
|
||||
(fn [_title _opts]
|
||||
(reset! created?* true)
|
||||
(p/resolved {:db/id 999
|
||||
:block/journal-day 20250102}))
|
||||
(constantly "Jan 2nd, 2025"))
|
||||
(p/then (fn [page]
|
||||
(is (= existing-page page))
|
||||
(is (false? @created?*))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
|
||||
(deftest resolve-journal-page-for-date-creates-page-when-missing-test
|
||||
(async done
|
||||
(let [created-page {:db/id 200
|
||||
:block/journal-day 20250102}
|
||||
created-calls* (atom [])]
|
||||
(-> (#'property-value/<resolve-journal-page-for-date
|
||||
(js/Date. "2025-01-02T00:00:00Z")
|
||||
(constantly "test-repo")
|
||||
(fn [_repo _title _opts]
|
||||
(p/resolved nil))
|
||||
(fn [title opts]
|
||||
(swap! created-calls* conj [title opts])
|
||||
(p/resolved created-page))
|
||||
(constantly "Jan 2nd, 2025"))
|
||||
(p/then (fn [page]
|
||||
(is (= created-page page))
|
||||
(is (= [["Jan 2nd, 2025" {:redirect? false}]] @created-calls*))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
|
||||
(deftest resolved-property-value-for-render-skips-default-for-placeholder-row-test
|
||||
(let [property {:db/ident :logseq.property/status
|
||||
:logseq.property/default-value {:db/id 3
|
||||
:db/ident :logseq.property/status.todo
|
||||
:block/title "Todo"}}
|
||||
placeholder-block {:db/id 1}]
|
||||
(is (nil? (#'property-value/resolved-property-value-for-render placeholder-block property false)))))
|
||||
|
||||
(deftest resolved-property-value-for-render-uses-default-for-loaded-block-test
|
||||
(let [property {:db/ident :logseq.property/status
|
||||
:logseq.property/default-value {:db/id 3
|
||||
:db/ident :logseq.property/status.todo
|
||||
:block/title "Todo"}}
|
||||
loaded-block {:db/id 1
|
||||
:block/uuid #uuid "11111111-1111-1111-1111-111111111111"}]
|
||||
(is (= (:logseq.property/default-value property)
|
||||
(#'property-value/resolved-property-value-for-render loaded-block property false)))))
|
||||
@@ -1,5 +1,5 @@
|
||||
(ns frontend.config-test
|
||||
(:require [cljs.test :refer [deftest is]]
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[frontend.config :as config]
|
||||
[frontend.state :as state]
|
||||
[logseq.common.config :as common-config]))
|
||||
@@ -8,3 +8,67 @@
|
||||
(with-redefs [state/state (atom {:system/info {:home-dir "/tmp/home"}})]
|
||||
(is (= "/tmp/home/logseq/graphs/foo~2Fbar"
|
||||
(config/get-local-dir (str common-config/db-version-prefix "foo/bar"))))))
|
||||
|
||||
(deftest custom-url->ws-url-test
|
||||
(testing "https URL becomes wss"
|
||||
(is (= "wss://my-server.example.com/sync/%s"
|
||||
(config/custom-url->ws-url "https://my-server.example.com"))))
|
||||
|
||||
(testing "http URL becomes ws"
|
||||
(is (= "ws://localhost:8787/sync/%s"
|
||||
(config/custom-url->ws-url "http://localhost:8787"))))
|
||||
|
||||
(testing "trailing slashes are stripped"
|
||||
(is (= "wss://my-server.example.com/sync/%s"
|
||||
(config/custom-url->ws-url "https://my-server.example.com/")))
|
||||
(is (= "wss://my-server.example.com/sync/%s"
|
||||
(config/custom-url->ws-url "https://my-server.example.com///"))))
|
||||
|
||||
(testing "preserves port in URL"
|
||||
(is (= "wss://example.com:3000/sync/%s"
|
||||
(config/custom-url->ws-url "https://example.com:3000"))))
|
||||
|
||||
(testing "preserves subpath in host"
|
||||
;; Users should only provide a base URL, but verify trailing path doesn't break things
|
||||
(is (= "wss://example.com/api/sync/%s"
|
||||
(config/custom-url->ws-url "https://example.com/api")))))
|
||||
|
||||
(deftest custom-url->http-base-test
|
||||
(testing "returns URL as-is when no trailing slash"
|
||||
(is (= "https://my-server.example.com"
|
||||
(config/custom-url->http-base "https://my-server.example.com"))))
|
||||
|
||||
(testing "strips trailing slashes"
|
||||
(is (= "https://my-server.example.com"
|
||||
(config/custom-url->http-base "https://my-server.example.com/")))
|
||||
(is (= "https://my-server.example.com"
|
||||
(config/custom-url->http-base "https://my-server.example.com///"))))
|
||||
|
||||
(testing "preserves http scheme"
|
||||
(is (= "http://localhost:8787"
|
||||
(config/custom-url->http-base "http://localhost:8787"))))
|
||||
|
||||
(testing "preserves port"
|
||||
(is (= "https://example.com:3000"
|
||||
(config/custom-url->http-base "https://example.com:3000/")))))
|
||||
|
||||
(deftest default-urls-are-returned-when-no-custom-url
|
||||
(testing "db-sync-ws-url returns default when no custom URL is set"
|
||||
;; In test environment, node-test? is true so get-custom-sync-server-url
|
||||
;; always returns nil, meaning we always get the default
|
||||
(is (string? (config/db-sync-ws-url)))
|
||||
(is (= config/default-db-sync-ws-url (config/db-sync-ws-url))))
|
||||
|
||||
(testing "db-sync-http-base returns default when no custom URL is set"
|
||||
(is (string? (config/db-sync-http-base)))
|
||||
(is (= config/default-db-sync-http-base (config/db-sync-http-base)))))
|
||||
|
||||
(deftest valid-sync-server-url?-test
|
||||
(testing "accepts http and https URLs"
|
||||
(is (config/valid-sync-server-url? "https://my-server.example.com"))
|
||||
(is (config/valid-sync-server-url? "http://localhost:8787")))
|
||||
|
||||
(testing "rejects non-URL strings"
|
||||
(is (not (config/valid-sync-server-url? "not a url")))
|
||||
(is (not (config/valid-sync-server-url? "")))
|
||||
(is (not (config/valid-sync-server-url? nil)))))
|
||||
|
||||
@@ -86,6 +86,20 @@
|
||||
(is (= 1 (:db/id (db/entity test-db 1))))
|
||||
(is (= 1 (:db/id (db/entity (conn/get-db test-db) 1)))))
|
||||
|
||||
(deftest get-latest-journals-should-exclude-recycled-journal-pages
|
||||
(load-test-files
|
||||
[{:page {:build/journal 20240101}}
|
||||
{:page {:build/journal 20240102}}
|
||||
{:page {:build/journal 20240103}}])
|
||||
(let [journal-2024-01-02-id (ffirst (d/q '[:find ?e
|
||||
:where
|
||||
[?e :block/journal-day 20240102]]
|
||||
(conn/get-db test-db)))]
|
||||
(db/transact! test-db [{:db/id journal-2024-01-02-id
|
||||
:logseq.property/deleted-at 1704196800000}]))
|
||||
(is (= [20240103 20240101]
|
||||
(mapv :block/journal-day (model/get-latest-journals test-db 10)))))
|
||||
|
||||
(deftest get-block-by-page-name-and-block-route-name
|
||||
(load-test-files
|
||||
[{:page {:block/title "foo"}
|
||||
|
||||
46
src/test/frontend/db/property_values_test.cljs
Normal file
46
src/test/frontend/db/property_values_test.cljs
Normal file
@@ -0,0 +1,46 @@
|
||||
(ns frontend.db.property-values-test
|
||||
(:require [cljs.test :refer [deftest is use-fixtures]]
|
||||
[datascript.core :as d]
|
||||
[frontend.db :as db]
|
||||
[frontend.test.helper :as test-helper]
|
||||
[logseq.db.common.entity-plus :as entity-plus]
|
||||
[logseq.db.common.view :as db-view]))
|
||||
|
||||
(def repo test-helper/test-db)
|
||||
|
||||
(defn start-and-destroy-db
|
||||
[f]
|
||||
(test-helper/start-and-destroy-db f))
|
||||
|
||||
(use-fixtures :each start-and-destroy-db)
|
||||
|
||||
(deftest get-property-values-filters-recycled-ref-values-test
|
||||
(let [property-ident :block/tags
|
||||
active-title "Active ref value"
|
||||
recycled-title "Recycled ref value"]
|
||||
(d/transact! (db/get-db repo false)
|
||||
[[:db/add -2 :block/title active-title]
|
||||
[:db/add -3 :block/title recycled-title]
|
||||
[:db/add -3 :logseq.property/deleted-at 1]
|
||||
[:db/add -10 property-ident -2]
|
||||
[:db/add -11 property-ident -3]])
|
||||
(let [result (db-view/get-property-values @(db/get-db repo false) property-ident {})]
|
||||
(is (contains? (set (map :label result)) active-title))
|
||||
(is (not (contains? (set (map :label result)) recycled-title))))))
|
||||
|
||||
(deftest property-closed-values-hide-recycled-values-test
|
||||
(d/transact! (db/get-db repo false)
|
||||
[{:db/id -1 :db/ident :user.property/closed-values-visibility}
|
||||
{:db/id -2
|
||||
:block/title "Visible closed value"
|
||||
:block/order "a"
|
||||
:block/closed-value-property -1}
|
||||
{:db/id -3
|
||||
:block/title "Recycled closed value"
|
||||
:block/order "b"
|
||||
:block/closed-value-property -1
|
||||
:logseq.property/deleted-at 1}])
|
||||
(let [db @(db/get-db repo false)
|
||||
property (d/entity db :user.property/closed-values-visibility)
|
||||
values (entity-plus/lookup-kv-then-entity property :property/closed-values)]
|
||||
(is (= ["Visible closed value"] (map :block/title values)))))
|
||||
36
src/test/frontend/handler/common/page_test.cljs
Normal file
36
src/test/frontend/handler/common/page_test.cljs
Normal file
@@ -0,0 +1,36 @@
|
||||
(ns frontend.handler.common.page-test
|
||||
(:require [clojure.test :refer [async is use-fixtures]]
|
||||
[datascript.core :as d]
|
||||
[frontend.db :as db]
|
||||
[frontend.handler.common.page :as page-common-handler]
|
||||
[frontend.test.helper :as test-helper :include-macros true :refer [deftest-async]]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.test.helper :as db-test]
|
||||
[logseq.outliner.page :as outliner-page]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(use-fixtures :each
|
||||
{:before (fn []
|
||||
(async done
|
||||
(test-helper/start-test-db!)
|
||||
(done)))
|
||||
:after test-helper/destroy-test-db!})
|
||||
|
||||
(deftest-async create-page-restores-recycled-page
|
||||
(test-helper/load-test-files [{:page {:block/title "foo"}
|
||||
:blocks [{:block/title "child block"}]}])
|
||||
(p/let [conn (db/get-db test-helper/test-db false)
|
||||
page (db-test/find-page-by-title @conn "foo")
|
||||
page-uuid (:block/uuid page)
|
||||
_ (outliner-page/delete! conn page-uuid {})
|
||||
recycled-page (d/entity @conn [:block/uuid page-uuid])
|
||||
_ (is (ldb/recycled? recycled-page)
|
||||
"Page should be recycled after deletion")
|
||||
restored-page (page-common-handler/<create! "foo" {:redirect? false})]
|
||||
(is (= (:db/id restored-page) (:db/id page))
|
||||
"create! should return the restored page")
|
||||
(let [page' (d/entity @conn [:block/uuid page-uuid])]
|
||||
(is (not (ldb/recycled? page'))
|
||||
"Page should no longer be recycled after re-creation")
|
||||
(is (= "foo" (get-in (db-test/find-block-by-content @conn "child block") [:block/page :block/title]))
|
||||
"Restored page still has its block(s)"))))
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -203,23 +203,28 @@
|
||||
(p/resolved {:error error}))))
|
||||
|
||||
(defn- with-fake-create-or-open-db
|
||||
[repo conn f]
|
||||
(let [thread-apis-prev @thread-api/*thread-apis]
|
||||
(vreset! thread-api/*thread-apis
|
||||
(assoc thread-apis-prev
|
||||
:thread-api/create-or-open-db
|
||||
(fn [_repo _opts]
|
||||
(swap! worker-state/*datascript-conns assoc repo conn)
|
||||
(p/resolved nil))
|
||||
:thread-api/db-sync-close-db
|
||||
(fn [_repo] nil)
|
||||
:thread-api/db-sync-invalidate-search-db
|
||||
(fn [_repo] (p/resolved nil))
|
||||
:thread-api/db-sync-rehydrate-large-titles
|
||||
(fn [_repo _graph-id] (p/resolved nil))))
|
||||
(-> (f)
|
||||
(p/finally (fn []
|
||||
(vreset! thread-api/*thread-apis thread-apis-prev))))))
|
||||
([repo conn f]
|
||||
(with-fake-create-or-open-db repo conn {} f))
|
||||
([repo conn
|
||||
{:keys [create-or-open-db-f close-db-f invalidate-search-db-f unlink-db-f]}
|
||||
f]
|
||||
(let [thread-apis-prev @thread-api/*thread-apis
|
||||
create-or-open-db-f (or create-or-open-db-f
|
||||
(fn [_repo _opts]
|
||||
(swap! worker-state/*datascript-conns assoc repo conn)
|
||||
(p/resolved nil)))
|
||||
close-db-f (or close-db-f (fn [_repo] nil))
|
||||
invalidate-search-db-f (or invalidate-search-db-f (fn [_repo] (p/resolved nil)))
|
||||
unlink-db-f (or unlink-db-f (fn [_repo] nil))]
|
||||
(vreset! thread-api/*thread-apis
|
||||
(assoc thread-apis-prev
|
||||
:thread-api/create-or-open-db create-or-open-db-f
|
||||
:thread-api/db-sync-close-db close-db-f
|
||||
:thread-api/db-sync-invalidate-search-db invalidate-search-db-f
|
||||
:thread-api/unsafe-unlink-db unlink-db-f))
|
||||
(-> (f)
|
||||
(p/finally (fn []
|
||||
(vreset! thread-api/*thread-apis thread-apis-prev)))))))
|
||||
|
||||
(deftest db-sync-import-prepare-replaces-active-import-state-test
|
||||
(async done
|
||||
@@ -243,6 +248,48 @@
|
||||
(is false (str error))
|
||||
(done)))))))))))
|
||||
|
||||
(deftest db-sync-import-prepare-reset-unlinks-db-before-reopen-test
|
||||
(async done
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [prepare (@thread-api/*thread-apis :thread-api/db-sync-import-prepare)
|
||||
conn (d/create-conn db-schema/schema)
|
||||
calls (atom [])]
|
||||
(with-fake-create-or-open-db
|
||||
test-repo conn
|
||||
{:close-db-f (fn [repo]
|
||||
(swap! calls conj [:close repo])
|
||||
nil)
|
||||
:unlink-db-f (fn [repo]
|
||||
(swap! calls conj [:unlink repo])
|
||||
nil)
|
||||
:invalidate-search-db-f (fn [repo]
|
||||
(swap! calls conj [:invalidate-search repo])
|
||||
(p/resolved nil))
|
||||
:create-or-open-db-f (fn [repo opts]
|
||||
(swap! calls conj [:create-or-open repo opts])
|
||||
(swap! worker-state/*datascript-conns assoc repo conn)
|
||||
(p/resolved nil))}
|
||||
(fn []
|
||||
(-> (prepare test-repo true "graph-1" false)
|
||||
(p/then (fn [_]
|
||||
(let [ops (mapv first @calls)
|
||||
idx (fn [op]
|
||||
(first (keep-indexed (fn [i v]
|
||||
(when (= op v) i))
|
||||
ops)))]
|
||||
(is (some? (idx :close)))
|
||||
(is (some? (idx :unlink)))
|
||||
(is (some? (idx :invalidate-search)))
|
||||
(is (some? (idx :create-or-open)))
|
||||
(is (< (idx :close) (idx :unlink)))
|
||||
(is (< (idx :unlink) (idx :invalidate-search)))
|
||||
(is (< (idx :invalidate-search) (idx :create-or-open))))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))))))
|
||||
|
||||
(deftest db-sync-import-finalize-rejects-stale-import-id-test
|
||||
(async done
|
||||
(restoring-worker-state
|
||||
@@ -336,6 +383,23 @@
|
||||
(is (< ident-idx data-idx))
|
||||
(is (< cardinality-idx data-idx)))))
|
||||
|
||||
(deftest import-datoms-batch-transacts-all-db-schema-before-data-test
|
||||
(async done
|
||||
(let [conn (d/create-conn db-schema/schema)
|
||||
attr-eid 8001
|
||||
datoms [{:e attr-eid :a :db/ident :v :user.test/indexed}
|
||||
{:e 100 :a :user.test/indexed :v "hello"}
|
||||
{:e attr-eid :a :db/valueType :v :db.type/string}
|
||||
{:e attr-eid :a :db/cardinality :v :db.cardinality/one}
|
||||
{:e attr-eid :a :db/index :v true}]]
|
||||
(-> (#'sync-download/import-datoms-batch! conn nil false datoms)
|
||||
(p/then (fn [_]
|
||||
(is (= 1 (count (d/datoms @conn :avet :user.test/indexed "hello"))))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
|
||||
(deftest thread-api-validate-db-passes-sync-diagnostics-test
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
@@ -386,3 +450,31 @@
|
||||
(finally
|
||||
(reset! db-sync/*repo->latest-remote-tx latest-tx-prev)
|
||||
(reset! db-sync/*repo->latest-remote-checksum latest-checksum-prev)))))))
|
||||
|
||||
(deftest thread-api-export-client-ops-db-checkpoints-and-exports-client-ops-file-test
|
||||
(async done
|
||||
(restoring-worker-state
|
||||
(fn []
|
||||
(let [export-client-ops-db (@thread-api/*thread-apis :thread-api/export-client-ops-db)
|
||||
sql-calls (atom [])
|
||||
export-calls (atom [])
|
||||
expected-data (js/Uint8Array. #js [1 2 3])
|
||||
expected-buffer (.-buffer expected-data)
|
||||
fake-pool #js {:exportFile (fn [path]
|
||||
(swap! export-calls conj path)
|
||||
expected-buffer)}]
|
||||
(reset! worker-state/*opfs-pools {test-repo fake-pool})
|
||||
(with-redefs [worker-state/get-sqlite-conn (fn [_repo which-db]
|
||||
(when (= :client-ops which-db)
|
||||
#js {:exec (fn [sql]
|
||||
(swap! sql-calls conj sql))}))]
|
||||
(-> (export-client-ops-db test-repo)
|
||||
(p/then (fn [result]
|
||||
(is (= ["PRAGMA wal_checkpoint(2)"] @sql-calls))
|
||||
(is (= ["client-ops/db.sqlite"] @export-calls))
|
||||
(is (instance? js/Uint8Array result))
|
||||
(is (= [1 2 3] (vec result)))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done))))))))))
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
[datascript.core :as d]
|
||||
[frontend.worker.pipeline :as worker-pipeline]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.db.common.order :as db-order]
|
||||
[logseq.db.test.helper :as db-test]))
|
||||
|
||||
(deftest test-built-in-page-updates-that-should-be-reverted
|
||||
@@ -143,3 +144,69 @@
|
||||
:block/title "page1-renamed"}])]
|
||||
(is (= "page1-renamed"
|
||||
(:block/title (d/entity (:db-after result) (:db/id page1)))))))))
|
||||
|
||||
(deftest built-in-tag-must-not-convert-page-child-block-to-class-test
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks [{:page {:block/title "page1"}}]})
|
||||
page1 (ldb/get-page @conn "page1")
|
||||
now (js/Date.now)
|
||||
bad-block-uuid (random-uuid)
|
||||
new-tag-uuid (random-uuid)]
|
||||
(ldb/register-transact-pipeline-fn! worker-pipeline/transact-pipeline)
|
||||
|
||||
(testing "page-child block with built-in #Tag stays a block"
|
||||
(ldb/transact! conn [{:block/uuid bad-block-uuid
|
||||
:block/title "charlie"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/page (:db/id page1)
|
||||
:block/parent (:db/id page1)
|
||||
:block/order (db-order/gen-key)
|
||||
:block/tags [:logseq.class/Tag]}])
|
||||
(let [block (d/entity @conn [:block/uuid bad-block-uuid])]
|
||||
(is (some? block))
|
||||
(is (nil? (:db/ident block)))
|
||||
(is (nil? (:logseq.property.class/extends block)))
|
||||
(is (not (ldb/class? block)))
|
||||
(is (= (:db/id page1) (:db/id (:block/parent block))))
|
||||
(is (empty? (:block/tags block)))))
|
||||
|
||||
(testing "standalone candidate is still converted to a class page"
|
||||
(ldb/transact! conn [{:block/uuid new-tag-uuid
|
||||
:block/name "standalone-tag"
|
||||
:block/title "standalone-tag"
|
||||
:block/created-at now
|
||||
:block/updated-at now
|
||||
:block/tags [:logseq.class/Tag]}])
|
||||
(let [tag-page (d/entity @conn [:block/uuid new-tag-uuid])]
|
||||
(is (ldb/class? tag-page))
|
||||
(is (keyword? (:db/ident tag-page)))
|
||||
(is (= "user.class" (namespace (:db/ident tag-page))))
|
||||
(is (= [:logseq.class/Root]
|
||||
(map :db/ident (:logseq.property.class/extends tag-page))))))
|
||||
|
||||
;; return global fn back to previous behavior
|
||||
(ldb/register-transact-pipeline-fn! identity)))
|
||||
|
||||
(deftest tag-template-insertion-resolves-dynamic-variable-test
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
{:pages-and-blocks
|
||||
[{:page {:block/title "Target Page"}
|
||||
:blocks [{:block/title "target block"}]}
|
||||
{:page {:block/title "Templates"}
|
||||
:blocks [{:block/title "tag template root"
|
||||
:build/children [{:block/title "auto <% current page %>"}]}]}]
|
||||
:classes {:DiaryEntry {}}})
|
||||
target-block (db-test/find-block-by-content @conn "target block")
|
||||
template-root (db-test/find-block-by-content @conn "tag template root")
|
||||
diary-entry (ldb/get-page @conn "DiaryEntry")]
|
||||
(ldb/transact! conn [[:db/add (:db/id template-root)
|
||||
:logseq.property/template-applied-to
|
||||
(:db/id diary-entry)]])
|
||||
(ldb/register-transact-pipeline-fn! worker-pipeline/transact-pipeline)
|
||||
(try
|
||||
(ldb/transact! conn [[:db/add (:db/id target-block) :block/tags (:db/id diary-entry)]])
|
||||
(is (some? (db-test/find-block-by-content @conn "auto [[Target Page]]")))
|
||||
(finally
|
||||
;; return global fn back to previous behavior
|
||||
(ldb/register-transact-pipeline-fn! identity)))))
|
||||
|
||||
@@ -19,3 +19,14 @@
|
||||
:logseq.property.reaction/target target-id}])
|
||||
affected (worker-react/get-affected-queries-keys tx-report)]
|
||||
(is (some #{[:frontend.worker.react/block-reactions target-id]} affected)))))
|
||||
|
||||
(deftest affected-keys-journals-when-journal-recycled
|
||||
(testing "recycling a journal page should refresh journals query key"
|
||||
(let [conn (db-test/create-conn-with-blocks
|
||||
[{:page {:build/journal 20240101}}
|
||||
{:page {:build/journal 20240102}}])
|
||||
journal (db-test/find-journal-by-journal-day @conn 20240102)
|
||||
tx-report (d/transact! conn [{:db/id (:db/id journal)
|
||||
:logseq.property/deleted-at 1704196800000}])
|
||||
affected (worker-react/get-affected-queries-keys tx-report)]
|
||||
(is (some #{[:frontend.worker.react/journals]} affected)))))
|
||||
|
||||
@@ -224,3 +224,38 @@
|
||||
(is (some? ctor))
|
||||
(is (= 1 (count result)))
|
||||
(is (= "alpha beta" (get-in result [0 :item :title])))))))
|
||||
|
||||
(deftest upsert-blocks-batches-rows-into-single-sql-statement
|
||||
(let [calls (atom [])
|
||||
tx #js {:exec (fn [opts]
|
||||
(swap! calls conj {:sql (aget opts "sql")
|
||||
:bind (js->clj (aget opts "bind"))}))}
|
||||
db #js {:transaction (fn [f] (f tx))}
|
||||
blocks (clj->js [{:id "67e55044-10b1-426f-9247-bb680e5fe0c8"
|
||||
:title "alpha"
|
||||
:page "67e55044-10b1-426f-9247-bb680e5fe0c8"}
|
||||
{:id "8f14e45f-ea6e-4be8-b53f-bf0f2ca8a5db"
|
||||
:title "beta"
|
||||
:page "8f14e45f-ea6e-4be8-b53f-bf0f2ca8a5db"}
|
||||
{:id "9d5ed678-fe57-4bcf-bf4d-6f2fd5f8995d"
|
||||
:title "gamma"
|
||||
:page "9d5ed678-fe57-4bcf-bf4d-6f2fd5f8995d"}])]
|
||||
(search/upsert-blocks! db blocks)
|
||||
(is (= 1 (count @calls)))
|
||||
(is (= "INSERT INTO blocks (id, title, page) VALUES (?, ?, ?), (?, ?, ?), (?, ?, ?) ON CONFLICT (id) DO UPDATE SET (title, page) = (excluded.title, excluded.page)"
|
||||
(:sql (first @calls))))
|
||||
(is (= ["67e55044-10b1-426f-9247-bb680e5fe0c8" "alpha" "67e55044-10b1-426f-9247-bb680e5fe0c8"
|
||||
"8f14e45f-ea6e-4be8-b53f-bf0f2ca8a5db" "beta" "8f14e45f-ea6e-4be8-b53f-bf0f2ca8a5db"
|
||||
"9d5ed678-fe57-4bcf-bf4d-6f2fd5f8995d" "gamma" "9d5ed678-fe57-4bcf-bf4d-6f2fd5f8995d"]
|
||||
(:bind (first @calls))))))
|
||||
|
||||
(deftest upsert-blocks-throws-on-invalid-input
|
||||
(let [tx #js {:exec (fn [_opts] nil)}
|
||||
db #js {:transaction (fn [f] (f tx))}
|
||||
error (try
|
||||
(search/upsert-blocks! db (clj->js [{:id "not-uuid" :title "alpha" :page "not-uuid"}]))
|
||||
nil
|
||||
(catch :default e e))]
|
||||
(is (some? error))
|
||||
(is (re-find #"Search upsert-blocks wrong data"
|
||||
(or (ex-message error) (str error))))))
|
||||
|
||||
@@ -1,49 +1,115 @@
|
||||
(ns frontend.worker.sync.client-op-test
|
||||
(:require [cljs.test :refer [deftest is testing]]
|
||||
[datascript.core :as d]
|
||||
[frontend.worker.state :as worker-state]
|
||||
[frontend.worker.sync.client-op :as client-op]))
|
||||
|
||||
(deftest update-graph-uuid-replaces-existing-value-test
|
||||
(let [repo "repo-1"
|
||||
conn (d/create-conn client-op/schema-in-db)
|
||||
(defn- new-memory-db
|
||||
[]
|
||||
(let [Database (js/require "better-sqlite3")]
|
||||
(new Database ":memory:")))
|
||||
|
||||
(defn- with-client-ops-db
|
||||
[repo f]
|
||||
(let [db (new-memory-db)
|
||||
prev-client-ops-conns @worker-state/*client-ops-conns]
|
||||
(reset! worker-state/*client-ops-conns {repo conn})
|
||||
(reset! worker-state/*client-ops-conns {repo db})
|
||||
(try
|
||||
(client-op/update-graph-uuid repo "graph-1")
|
||||
(client-op/update-graph-uuid repo "graph-2")
|
||||
(let [graph-uuid-datoms (vec (d/datoms @conn :avet :graph-uuid))]
|
||||
(is (= 1 (count graph-uuid-datoms)))
|
||||
(is (= #{"graph-2"} (set (map :v graph-uuid-datoms)))))
|
||||
(f db)
|
||||
(finally
|
||||
(.close db)
|
||||
(reset! worker-state/*client-ops-conns prev-client-ops-conns)))))
|
||||
|
||||
(defn- sqlite-count
|
||||
[^js db sql & args]
|
||||
(let [^js stmt (.prepare db sql)
|
||||
^js row (if (seq args)
|
||||
(.apply (.-get stmt) stmt (to-array args))
|
||||
(.get stmt))]
|
||||
(if row
|
||||
(or (aget row "c")
|
||||
(aget row "count"))
|
||||
0)))
|
||||
|
||||
(deftest sqlite-sync-meta-roundtrip-test
|
||||
(let [repo "repo-1"]
|
||||
(with-client-ops-db
|
||||
repo
|
||||
(fn [_db]
|
||||
(client-op/update-graph-uuid repo "graph-1")
|
||||
(client-op/update-local-tx repo 9)
|
||||
(client-op/update-local-checksum repo "checksum-1")
|
||||
|
||||
(client-op/update-graph-uuid repo "graph-2")
|
||||
(client-op/update-local-tx repo 12)
|
||||
(client-op/update-local-checksum repo "checksum-2")
|
||||
|
||||
(is (= "graph-2" (client-op/get-graph-uuid repo)))
|
||||
(is (= 12 (client-op/get-local-tx repo)))
|
||||
(is (= "checksum-2" (client-op/get-local-checksum repo)))))))
|
||||
|
||||
(deftest sqlite-asset-ops-coalescing-test
|
||||
(let [repo "repo-asset"
|
||||
asset-uuid (random-uuid)]
|
||||
(with-client-ops-db
|
||||
repo
|
||||
(fn [_db]
|
||||
(client-op/add-asset-ops repo [[:update-asset 10 {:block-uuid asset-uuid}]])
|
||||
(is (= 1 (client-op/get-unpushed-asset-ops-count repo)))
|
||||
(is (= [:update-asset 10 {:block-uuid asset-uuid}]
|
||||
(:update-asset (first (client-op/get-all-asset-ops repo)))))
|
||||
|
||||
;; older remove should be ignored because a newer update already exists
|
||||
(client-op/add-asset-ops repo [[:remove-asset 9 {:block-uuid asset-uuid}]])
|
||||
(is (= [:update-asset 10 {:block-uuid asset-uuid}]
|
||||
(:update-asset (first (client-op/get-all-asset-ops repo)))))
|
||||
|
||||
;; newer remove should replace update
|
||||
(client-op/add-asset-ops repo [[:remove-asset 11 {:block-uuid asset-uuid}]])
|
||||
(is (= [:remove-asset 11 {:block-uuid asset-uuid}]
|
||||
(:remove-asset (first (client-op/get-all-asset-ops repo)))))
|
||||
|
||||
(client-op/remove-asset-op repo asset-uuid)
|
||||
(is (= 0 (client-op/get-unpushed-asset-ops-count repo)))))))
|
||||
|
||||
(deftest cleanup-finished-history-ops-removes-only-unreferenced-finished-txs-test
|
||||
(let [repo "repo-cleanup"
|
||||
conn (d/create-conn client-op/schema-in-db)
|
||||
prev-client-ops-conns @worker-state/*client-ops-conns
|
||||
keep-tx-id (random-uuid)
|
||||
remove-tx-id (random-uuid)
|
||||
pending-tx-id (random-uuid)]
|
||||
(reset! worker-state/*client-ops-conns {repo conn})
|
||||
(try
|
||||
(d/transact! conn
|
||||
[{:db-sync/tx-id keep-tx-id
|
||||
:db-sync/pending? false}
|
||||
{:db-sync/tx-id remove-tx-id
|
||||
:db-sync/pending? false}
|
||||
{:db-sync/tx-id pending-tx-id
|
||||
:db-sync/pending? true}
|
||||
{:db-ident :metadata/local
|
||||
:local-tx 99}])
|
||||
(with-client-ops-db
|
||||
repo
|
||||
(fn [db]
|
||||
(client-op/update-local-tx repo 99)
|
||||
(client-op/upsert-local-tx-entry!
|
||||
repo
|
||||
{:tx-id keep-tx-id
|
||||
:created-at 1
|
||||
:pending? false
|
||||
:failed? false
|
||||
:normalized-tx-data []
|
||||
:reversed-tx-data []})
|
||||
(client-op/upsert-local-tx-entry!
|
||||
repo
|
||||
{:tx-id remove-tx-id
|
||||
:created-at 2
|
||||
:pending? false
|
||||
:failed? false
|
||||
:normalized-tx-data []
|
||||
:reversed-tx-data []})
|
||||
(client-op/upsert-local-tx-entry!
|
||||
repo
|
||||
{:tx-id pending-tx-id
|
||||
:created-at 3
|
||||
:pending? true
|
||||
:failed? false
|
||||
:normalized-tx-data []
|
||||
:reversed-tx-data []})
|
||||
|
||||
(is (= 1 (client-op/cleanup-finished-history-ops! repo #{keep-tx-id})))
|
||||
(is (some? (d/entity @conn [:db-sync/tx-id keep-tx-id])))
|
||||
(is (nil? (d/entity @conn [:db-sync/tx-id remove-tx-id])))
|
||||
(is (some? (d/entity @conn [:db-sync/tx-id pending-tx-id])))
|
||||
(is (= 99 (:local-tx (d/entity @conn [:db-ident :metadata/local]))))
|
||||
(finally
|
||||
(reset! worker-state/*client-ops-conns prev-client-ops-conns)))))
|
||||
(is (= 1 (client-op/cleanup-finished-history-ops! repo #{keep-tx-id})))
|
||||
(is (= 1 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str keep-tx-id))))
|
||||
(is (= 0 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str remove-tx-id))))
|
||||
(is (= 1 (sqlite-count db "select count(*) as c from client_ops where tx_id = ?" (str pending-tx-id))))
|
||||
(is (= 99 (client-op/get-local-tx repo)))))))
|
||||
|
||||
(deftest cleanup-finished-history-ops-no-conn-is-noop-test
|
||||
(let [repo "repo-no-conn"
|
||||
|
||||
@@ -595,3 +595,16 @@
|
||||
(is (= "decrypt-aes-key" (ex-message e)))
|
||||
(is (zero? @clear-user-rsa-cache-calls*))
|
||||
(done)))))))
|
||||
|
||||
(deftest decrypt-text-value-legacy-plaintext-test
|
||||
(async done
|
||||
(-> (p/let [aes-key (crypt/<generate-aes-key)
|
||||
plaintext "$$$favorites"
|
||||
encrypted (crypt/<encrypt-uint8array aes-key (.encode (js/TextEncoder.) plaintext))
|
||||
encrypted-str (ldb/write-transit-str encrypted)
|
||||
decrypted (sync-crypt/<decrypt-text-value aes-key encrypted-str)]
|
||||
(is (= plaintext decrypted))
|
||||
(done))
|
||||
(p/catch (fn [e]
|
||||
(is false (str e))
|
||||
(done))))))
|
||||
|
||||
44
src/test/frontend/worker/sync/download_test.cljs
Normal file
44
src/test/frontend/worker/sync/download_test.cljs
Normal file
@@ -0,0 +1,44 @@
|
||||
(ns frontend.worker.sync.download-test
|
||||
(:require [cljs.test :refer [async deftest is]]
|
||||
[frontend.worker.sync.download :as sync-download]
|
||||
[logseq.db-sync.snapshot :as snapshot]
|
||||
[promesa.core :as p]))
|
||||
|
||||
(defn- frame-bytes
|
||||
[^js data]
|
||||
(let [len (.-byteLength data)
|
||||
out (js/Uint8Array. (+ 4 len))
|
||||
view (js/DataView. (.-buffer out))]
|
||||
(.setUint32 view 0 len false)
|
||||
(.set out data 4)
|
||||
out))
|
||||
|
||||
(defn- stream-from-payload
|
||||
[^js payload]
|
||||
(js/ReadableStream.
|
||||
#js {:start (fn [controller]
|
||||
(.enqueue controller payload)
|
||||
(.close controller))}))
|
||||
|
||||
(deftest stream-snapshot-row-batches-ignores-stale-gzip-header-test
|
||||
(async done
|
||||
(let [rows [[1 "row-1" nil]
|
||||
[2 "row-2" nil]]
|
||||
payload (frame-bytes (snapshot/encode-rows rows))
|
||||
resp (js/Response.
|
||||
(stream-from-payload payload)
|
||||
#js {:status 200
|
||||
:headers #js {"content-encoding" "gzip"}})
|
||||
batches* (atom [])]
|
||||
(-> (#'sync-download/<stream-snapshot-row-batches!
|
||||
resp
|
||||
1000
|
||||
(fn [batch]
|
||||
(swap! batches* conj batch)
|
||||
(p/resolved true)))
|
||||
(p/then (fn [_]
|
||||
(is (= [rows] @batches*))
|
||||
(done)))
|
||||
(p/catch (fn [error]
|
||||
(is false (str error))
|
||||
(done)))))))
|
||||
@@ -14,6 +14,24 @@
|
||||
|
||||
(def ^:private test-repo "test-worker-undo-redo")
|
||||
|
||||
(defn- new-client-ops-db
|
||||
[]
|
||||
(let [Database (js/require "better-sqlite3")
|
||||
db (new Database ":memory:")]
|
||||
(client-op/ensure-sqlite-schema! db)
|
||||
db))
|
||||
|
||||
(defn- delete-client-op-tx-row!
|
||||
[^js db tx-id]
|
||||
(let [^js stmt (.prepare db "delete from client_ops where kind = 'tx' and tx_id = ?")]
|
||||
(.run stmt (str tx-id))))
|
||||
|
||||
(defn- client-op-tx-row-exists?
|
||||
[^js db tx-id]
|
||||
(let [^js stmt (.prepare db "select 1 as ok from client_ops where kind = 'tx' and tx_id = ? limit 1")
|
||||
row (.get stmt (str tx-id))]
|
||||
(some? row)))
|
||||
|
||||
(defn- local-tx-meta
|
||||
[m]
|
||||
(assoc m
|
||||
@@ -31,7 +49,7 @@
|
||||
:blocks [{:block/title "task"}
|
||||
{:block/title "parent"
|
||||
:build/children [{:block/title "child"}]}]}]})
|
||||
client-ops-conn (d/create-conn client-op/schema-in-db)]
|
||||
client-ops-conn (new-client-ops-db)]
|
||||
(reset! worker-state/*datascript-conns {test-repo conn})
|
||||
(reset! worker-state/*client-ops-conns {test-repo client-ops-conn})
|
||||
(reset! worker-undo-redo/*apply-history-action! sync-apply/apply-history-action!)
|
||||
@@ -44,6 +62,7 @@
|
||||
(finally
|
||||
(d/unlisten! conn ::gen-undo-ops)
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(.close client-ops-conn)
|
||||
(reset! worker-undo-redo/*apply-history-action! apply-history-action-prev)
|
||||
(reset! worker-state/*datascript-conns datascript-prev)
|
||||
(reset! worker-state/*client-ops-conns client-ops-prev)))))
|
||||
@@ -89,6 +108,121 @@
|
||||
:outliner-ops [[:save-block [{:block/uuid block-uuid
|
||||
:block/title title} {}]]]}))))
|
||||
|
||||
(defn- block-id->uuid
|
||||
[db block-id]
|
||||
(cond
|
||||
(uuid? block-id)
|
||||
block-id
|
||||
|
||||
(and (vector? block-id) (= :block/uuid (first block-id)))
|
||||
(second block-id)
|
||||
|
||||
(number? block-id)
|
||||
(or (some-> (d/entity db block-id) :block/uuid)
|
||||
block-id)
|
||||
|
||||
:else
|
||||
block-id))
|
||||
|
||||
(defn- property-id->ident
|
||||
[db property-id]
|
||||
(cond
|
||||
(qualified-keyword? property-id)
|
||||
property-id
|
||||
|
||||
(number? property-id)
|
||||
(or (some-> (d/entity db property-id) :db/ident)
|
||||
property-id)
|
||||
|
||||
:else
|
||||
property-id))
|
||||
|
||||
(defn- normalize-op-block-ids
|
||||
[db [op args :as op-entry]]
|
||||
(let [id (fn [v] (block-id->uuid db v))
|
||||
property-id (fn [v] (property-id->ident db v))
|
||||
ids (fn [vs] (mapv id vs))]
|
||||
(case op
|
||||
:save-block
|
||||
(let [[block opts] args
|
||||
block' (cond-> block
|
||||
(and (map? block)
|
||||
(uuid? (:db/id block)))
|
||||
((fn [m]
|
||||
(cond-> (dissoc m :db/id)
|
||||
(nil? (:block/uuid m))
|
||||
(assoc :block/uuid (:db/id m)))))
|
||||
(and (map? block)
|
||||
(nil? (:block/uuid block))
|
||||
(number? (:db/id block)))
|
||||
(assoc :block/uuid (id (:db/id block))))]
|
||||
[op [block' opts]])
|
||||
|
||||
:insert-blocks
|
||||
[op [(first args) (id (second args)) (nth args 2)]]
|
||||
|
||||
:apply-template
|
||||
[op [(id (first args)) (id (second args)) (nth args 2)]]
|
||||
|
||||
:delete-blocks
|
||||
[op [(ids (first args)) (second args)]]
|
||||
|
||||
:move-blocks
|
||||
[op [(ids (first args)) (id (second args)) (nth args 2)]]
|
||||
|
||||
:move-blocks-up-down
|
||||
[op [(ids (first args)) (second args)]]
|
||||
|
||||
:indent-outdent-blocks
|
||||
[op [(ids (first args)) (second args) (nth args 2)]]
|
||||
|
||||
:set-block-property
|
||||
[op [(id (first args)) (property-id (second args)) (nth args 2)]]
|
||||
|
||||
:remove-block-property
|
||||
[op [(id (first args)) (property-id (second args))]]
|
||||
|
||||
:delete-property-value
|
||||
[op [(id (first args)) (property-id (second args)) (nth args 2)]]
|
||||
|
||||
:create-property-text-block
|
||||
[op [(some-> (first args) id) (property-id (second args)) (nth args 2) (nth args 3)]]
|
||||
|
||||
:batch-set-property
|
||||
[op [(ids (first args)) (property-id (second args)) (nth args 2) (nth args 3)]]
|
||||
|
||||
:batch-remove-property
|
||||
[op [(ids (first args)) (property-id (second args))]]
|
||||
|
||||
:batch-delete-property-value
|
||||
[op [(ids (first args)) (property-id (second args)) (nth args 2)]]
|
||||
|
||||
:class-add-property
|
||||
[op [(id (first args)) (property-id (second args))]]
|
||||
|
||||
:class-remove-property
|
||||
[op [(id (first args)) (property-id (second args))]]
|
||||
|
||||
:upsert-property
|
||||
[op [(some-> (first args) property-id) (second args) (nth args 2)]]
|
||||
|
||||
:upsert-closed-value
|
||||
[op [(property-id (first args)) (second args)]]
|
||||
|
||||
:delete-closed-value
|
||||
[op [(property-id (first args)) (id (second args))]]
|
||||
|
||||
:add-existing-values-to-closed-values
|
||||
[op [(property-id (first args)) (second args)]]
|
||||
|
||||
op-entry)))
|
||||
|
||||
(defn- apply-ops!
|
||||
[conn ops opts]
|
||||
(outliner-op/apply-ops! conn
|
||||
(mapv #(normalize-op-block-ids @conn %) ops)
|
||||
opts))
|
||||
|
||||
(deftest undo-redo-selection-editor-info-roundtrip-test
|
||||
(testing "undo/redo result keeps block selection editor info when no cursor is recorded"
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
@@ -140,6 +274,43 @@
|
||||
(second %))
|
||||
redo-op)))
|
||||
|
||||
(defn- move-retract-entity-ops-to-front
|
||||
[tx-data]
|
||||
(let [retract-entity-op? (fn [item]
|
||||
(and (vector? item)
|
||||
(= 2 (count item))
|
||||
(= :db/retractEntity (first item))))
|
||||
retract-ops (filter retract-entity-op? tx-data)
|
||||
others (remove retract-entity-op? tx-data)]
|
||||
(vec (concat retract-ops others))))
|
||||
|
||||
(defn- poison-history-tx-order!
|
||||
[tx-id]
|
||||
(when-let [entry (client-op/get-local-tx-entry test-repo tx-id)]
|
||||
(client-op/upsert-local-tx-entry!
|
||||
test-repo
|
||||
{:tx-id tx-id
|
||||
:pending? true
|
||||
:failed? false
|
||||
:outliner-op (:outliner-op entry)
|
||||
:undo-redo (:db-sync/undo-redo entry)
|
||||
:forward-outliner-ops (:forward-outliner-ops entry)
|
||||
:inverse-outliner-ops (:inverse-outliner-ops entry)
|
||||
:inferred-outliner-ops? (:inferred-outliner-ops? entry)
|
||||
:normalized-tx-data (move-retract-entity-ops-to-front (:tx entry))
|
||||
:reversed-tx-data (move-retract-entity-ops-to-front (:reversed-tx entry))})))
|
||||
|
||||
(defn- property-value-titles
|
||||
[value]
|
||||
(cond
|
||||
(nil? value) []
|
||||
(string? value) [value]
|
||||
(map? value) [(:block/title value)]
|
||||
(coll? value) (->> value
|
||||
(mapcat property-value-titles)
|
||||
vec)
|
||||
:else [value]))
|
||||
|
||||
(deftest undo-missing-history-action-row-replays-from-inline-ops-test
|
||||
(testing "undo/redo should replay from inline history ops when pending row is missing"
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
@@ -168,8 +339,7 @@
|
||||
:tx-data [(d/datom 1 :block/title "poisoned" 1 true)])]
|
||||
item))
|
||||
op)))))
|
||||
(when-let [tx-ent (d/entity @client-ops-conn [:db-sync/tx-id tx-id-2])]
|
||||
(ldb/transact! client-ops-conn [[:db/retractEntity (:db/id tx-ent)]]))
|
||||
(delete-client-op-tx-row! client-ops-conn tx-id-2)
|
||||
(let [undo-result (worker-undo-redo/undo test-repo)]
|
||||
(is (not= ::worker-undo-redo/empty-undo-stack undo-result))
|
||||
(is (= "v1" (:block/title (d/entity @conn [:block/uuid child-uuid]))))
|
||||
@@ -246,8 +416,7 @@
|
||||
(assoc (second item) :tx-data [(d/datom 1 :block/title "poisoned" 1 true)])]
|
||||
item))
|
||||
op)))))
|
||||
(when-let [tx-ent (d/entity @client-ops-conn [:db-sync/tx-id tx-id])]
|
||||
(ldb/transact! client-ops-conn [[:db/retractEntity (:db/id tx-ent)]]))
|
||||
(delete-client-op-tx-row! client-ops-conn tx-id)
|
||||
(is (not= ::worker-undo-redo/empty-undo-stack
|
||||
(worker-undo-redo/undo test-repo)))
|
||||
(is (seq (get @worker-undo-redo/*redo-ops test-repo))))))
|
||||
@@ -271,7 +440,7 @@
|
||||
(let [undo-tx-id (:db-sync/tx-id (latest-undo-history-data))]
|
||||
(is (uuid? undo-tx-id))
|
||||
(is (not= source-tx-id undo-tx-id))
|
||||
(is (some? (d/entity @client-ops-conn [:db-sync/tx-id undo-tx-id])))))))))
|
||||
(is (client-op-tx-row-exists? client-ops-conn undo-tx-id))))))))
|
||||
|
||||
(deftest undo-records-only-local-txs-test
|
||||
(testing "undo history records only local txs"
|
||||
@@ -314,7 +483,7 @@
|
||||
(get-in data [:db-sync/inverse-outliner-ops 0 1 0 :block/uuid])))))))
|
||||
|
||||
(deftest undo-history-allows-non-semantic-outliner-op-test
|
||||
(testing "non-semantic outliner-op with transact placeholder is skipped by ops-only undo history"
|
||||
(testing "non-semantic outliner-op with transact placeholder is persisted in undo history"
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(let [conn (worker-state/get-datascript-conn test-repo)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
@@ -322,9 +491,13 @@
|
||||
[[:db/add [:block/uuid child-uuid] :block/title "restored child"]]
|
||||
(local-tx-meta
|
||||
{:client-id "test-client"
|
||||
:outliner-op :restore-recycled
|
||||
:outliner-ops [[:transact nil]]}))
|
||||
(is (empty? (get @worker-undo-redo/*undo-ops test-repo))))))
|
||||
:outliner-op :restore-recycled}))
|
||||
(let [undo-op (last (get @worker-undo-redo/*undo-ops test-repo))
|
||||
data (some #(when (= ::worker-undo-redo/db-transact (first %))
|
||||
(second %))
|
||||
undo-op)]
|
||||
(is (some? data))
|
||||
(is (nil? (:db-sync/inverse-outliner-ops data)))))))
|
||||
|
||||
(deftest undo-history-canonicalizes-insert-block-uuids-test
|
||||
(testing "worker undo history uses the created block uuid for insert semantic ops"
|
||||
@@ -360,7 +533,7 @@
|
||||
(is (= inserted-uuid
|
||||
(get-in data [:db-sync/forward-outliner-ops 0 1 0 0 :block/uuid])))
|
||||
(is (= inserted-uuid
|
||||
(second (first (get-in data [:db-sync/inverse-outliner-ops 0 1 0])))))))))
|
||||
(get-in data [:db-sync/inverse-outliner-ops 0 1 0 0])))))))
|
||||
|
||||
(deftest undo-works-for-local-graph-test
|
||||
(testing "worker undo/redo works for local changes on local graph"
|
||||
@@ -375,12 +548,36 @@
|
||||
(is (map? redo-result))
|
||||
(is (= "local-1" (:block/title (d/entity @conn [:block/uuid child-uuid]))))))))
|
||||
|
||||
(deftest undo-cycle-todo-removes-task-class-test
|
||||
(testing "undoing first status set should remove task class and status"
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(let [conn (worker-state/get-datascript-conn test-repo)
|
||||
block-uuid (:block/uuid (db-test/find-block-by-content @conn "task"))]
|
||||
(apply-ops! conn
|
||||
[[:set-block-property [block-uuid
|
||||
:logseq.property/status
|
||||
:logseq.property/status.todo]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
|
||||
(let [block-after-set (d/entity @conn [:block/uuid block-uuid])]
|
||||
(is (= :logseq.property/status.todo
|
||||
(some-> (:logseq.property/status block-after-set) :db/ident)))
|
||||
(is (contains? (set (map :db/ident (:block/tags block-after-set)))
|
||||
:logseq.class/Task)))
|
||||
|
||||
(is (map? (worker-undo-redo/undo test-repo)))
|
||||
(let [block-after-undo (d/entity @conn [:block/uuid block-uuid])]
|
||||
(is (not (contains? (d/pull @conn [:logseq.property/status] [:block/uuid block-uuid])
|
||||
:logseq.property/status)))
|
||||
(is (not (contains? (set (map :db/ident (:block/tags block-after-undo)))
|
||||
:logseq.class/Task)))))))
|
||||
|
||||
(deftest undo-delete-page-restores-page-out-of-recycle-test
|
||||
(testing "undoing delete-page should restore page and clear recycle marker"
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(let [conn (worker-state/get-datascript-conn test-repo)
|
||||
{:keys [page-uuid]} (seed-page-parent-child!)]
|
||||
(outliner-op/apply-ops! conn
|
||||
(apply-ops! conn
|
||||
[[:delete-page [page-uuid {}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(let [deleted-page (d/entity @conn [:block/uuid page-uuid])]
|
||||
@@ -401,14 +598,14 @@
|
||||
(testing "undoing delete-page restores hard-retracted class/property pages and today page blocks"
|
||||
(let [conn (worker-state/get-datascript-conn test-repo)
|
||||
class-title "undo class page movie"
|
||||
[_ class-uuid] (outliner-op/apply-ops! conn
|
||||
[_ class-uuid] (apply-ops! conn
|
||||
[[:create-page [class-title
|
||||
{:class? true
|
||||
:redirect? false
|
||||
:split-namespace? true
|
||||
:tags ()}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
_ (outliner-op/apply-ops! conn
|
||||
_ (apply-ops! conn
|
||||
[[:upsert-property [:user.property/undo-rating
|
||||
{:logseq.property/type :number}
|
||||
{:property-name "undo-rating"}]]]
|
||||
@@ -420,7 +617,7 @@
|
||||
today-day
|
||||
(:logseq.property.journal/title-format
|
||||
(d/entity @conn :logseq.class/Journal)))
|
||||
[_ today-page-uuid] (outliner-op/apply-ops! conn
|
||||
[_ today-page-uuid] (apply-ops! conn
|
||||
[[:create-page [today-title
|
||||
{:today-journal? true
|
||||
:redirect? false
|
||||
@@ -429,7 +626,7 @@
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
today-page-id (:db/id (d/entity @conn [:block/uuid today-page-uuid]))
|
||||
today-child-uuid (random-uuid)
|
||||
_ (outliner-op/apply-ops! conn
|
||||
_ (apply-ops! conn
|
||||
[[:insert-blocks [[{:block/uuid today-child-uuid
|
||||
:block/title "today undo child"}]
|
||||
today-page-id
|
||||
@@ -440,7 +637,7 @@
|
||||
property-ident-before (:db/ident (d/entity @conn [:block/uuid property-uuid]))]
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
|
||||
(outliner-op/apply-ops! conn
|
||||
(apply-ops! conn
|
||||
[[:delete-page [class-uuid {}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(is (nil? (d/entity @conn [:block/uuid class-uuid])))
|
||||
@@ -449,7 +646,7 @@
|
||||
(:db/ident (d/entity @conn [:block/uuid class-uuid]))))
|
||||
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(outliner-op/apply-ops! conn
|
||||
(apply-ops! conn
|
||||
[[:delete-page [property-uuid {}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(is (nil? (d/entity @conn :user.property/undo-rating)))
|
||||
@@ -458,7 +655,7 @@
|
||||
(:db/ident (d/entity @conn [:block/uuid property-uuid]))))
|
||||
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(outliner-op/apply-ops! conn
|
||||
(apply-ops! conn
|
||||
[[:delete-page [today-page-uuid {}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(is (some? (d/entity @conn [:block/uuid today-page-uuid])))
|
||||
@@ -471,7 +668,7 @@
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(let [conn (worker-state/get-datascript-conn test-repo)
|
||||
page-title "redo create page alpha"]
|
||||
(outliner-op/apply-ops! conn
|
||||
(apply-ops! conn
|
||||
[[:create-page [page-title {:redirect? false
|
||||
:split-namespace? true
|
||||
:tags ()}]]]
|
||||
@@ -500,7 +697,7 @@
|
||||
template-a-uuid (random-uuid)
|
||||
template-b-uuid (random-uuid)
|
||||
empty-target-uuid (random-uuid)]
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [[{:block/uuid template-root-uuid
|
||||
:block/title "template 1"
|
||||
@@ -515,7 +712,7 @@
|
||||
{:sibling? false
|
||||
:keep-uuid? true}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [[{:block/uuid empty-target-uuid
|
||||
:block/title ""}]
|
||||
@@ -531,7 +728,7 @@
|
||||
blocks-to-insert (cons (assoc (first template-blocks)
|
||||
:logseq.property/used-template (:db/id template-root))
|
||||
(rest template-blocks))]
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [blocks-to-insert
|
||||
(:db/id empty-target)
|
||||
@@ -566,7 +763,7 @@
|
||||
template-a-uuid (random-uuid)
|
||||
template-b-uuid (random-uuid)
|
||||
empty-target-uuid (random-uuid)]
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [[{:block/uuid template-root-uuid
|
||||
:block/title "template 1"
|
||||
@@ -581,7 +778,7 @@
|
||||
{:sibling? false
|
||||
:keep-uuid? true}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [[{:block/uuid empty-target-uuid
|
||||
:block/title ""}]
|
||||
@@ -597,7 +794,7 @@
|
||||
blocks-to-insert (cons (assoc (first template-blocks)
|
||||
:logseq.property/used-template (:db/id template-root))
|
||||
(rest template-blocks))]
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [blocks-to-insert
|
||||
(:db/id empty-target)
|
||||
@@ -636,7 +833,7 @@
|
||||
empty-target-uuid (random-uuid)
|
||||
inserted-root-uuid (random-uuid)
|
||||
inserted-child-uuid (random-uuid)]
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [[{:block/uuid empty-target-uuid
|
||||
:block/title ""}]
|
||||
@@ -645,7 +842,7 @@
|
||||
:keep-uuid? true}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(let [empty-target (d/entity @conn [:block/uuid empty-target-uuid])]
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [[{:block/uuid inserted-root-uuid
|
||||
:block/title "insert root"}
|
||||
@@ -688,7 +885,7 @@
|
||||
template-a-uuid (random-uuid)
|
||||
template-b-uuid (random-uuid)
|
||||
empty-target-uuid (random-uuid)]
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [[{:block/uuid template-root-uuid
|
||||
:block/title "template 1"
|
||||
@@ -703,7 +900,7 @@
|
||||
{:sibling? false
|
||||
:keep-uuid? true}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [[{:block/uuid empty-target-uuid
|
||||
:block/title ""}]
|
||||
@@ -719,7 +916,7 @@
|
||||
blocks-to-insert (cons (assoc (first template-blocks)
|
||||
:logseq.property/used-template (:db/id template-root))
|
||||
(rest template-blocks))]
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:apply-template [(:db/id template-root)
|
||||
(:db/id empty-target)
|
||||
@@ -757,7 +954,7 @@
|
||||
template-a-uuid (random-uuid)
|
||||
template-b-uuid (random-uuid)
|
||||
empty-target-uuid (random-uuid)]
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [[{:block/uuid template-root-uuid
|
||||
:block/title "template 1"
|
||||
@@ -772,7 +969,7 @@
|
||||
{:sibling? false
|
||||
:keep-uuid? true}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:insert-blocks [[{:block/uuid empty-target-uuid
|
||||
:block/title ""}]
|
||||
@@ -798,7 +995,7 @@
|
||||
[?b :block/title "a"]]
|
||||
@conn
|
||||
template-root-uuid))]
|
||||
(outliner-op/apply-ops!
|
||||
(apply-ops!
|
||||
conn
|
||||
[[:apply-template [(:db/id template-root)
|
||||
(:db/id empty-target)
|
||||
@@ -831,7 +1028,7 @@
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(let [conn (worker-state/get-datascript-conn test-repo)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(outliner-op/apply-ops! conn
|
||||
(apply-ops! conn
|
||||
[[:save-block [{:block/uuid child-uuid
|
||||
:block/title "saved via apply-ops"} {}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
@@ -898,7 +1095,7 @@
|
||||
(let [conn (worker-state/get-datascript-conn test-repo)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(doseq [title ["foo" "foo bar"]]
|
||||
(outliner-op/apply-ops! conn
|
||||
(apply-ops! conn
|
||||
[[:save-block [{:block/uuid child-uuid
|
||||
:block/title title} {}]]]
|
||||
(local-tx-meta {:client-id "test-client"})))
|
||||
@@ -908,6 +1105,40 @@
|
||||
(worker-undo-redo/redo test-repo)
|
||||
(is (= "foo bar" (:block/title (d/entity @conn [:block/uuid child-uuid])))))))
|
||||
|
||||
(deftest repeated-set-block-property-text-value-undo-redo-test
|
||||
(testing "set-block-property text value survives repeated undo/redo for one and many cardinalities"
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(let [conn (worker-state/get-datascript-conn test-repo)
|
||||
{:keys [child-uuid]} (seed-page-parent-child!)]
|
||||
(doseq [[suffix cardinality] [[:one :one] [:many :many]]]
|
||||
(let [property-id (keyword (str "user.property/p1-undo-redo-" (name suffix)))]
|
||||
(apply-ops! conn
|
||||
[[:upsert-property [property-id
|
||||
{:logseq.property/type :default
|
||||
:db/cardinality cardinality}
|
||||
{}]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
(apply-ops! conn
|
||||
[[:set-block-property [child-uuid
|
||||
property-id
|
||||
"value-1"]]]
|
||||
(local-tx-meta {:client-id "test-client"}))
|
||||
(let [history (latest-undo-history-data)]
|
||||
(is (empty? (:db-sync/forward-outliner-ops history))))
|
||||
(dotimes [_ 3]
|
||||
(when-let [undo-tx-id (:db-sync/tx-id (latest-undo-history-data))]
|
||||
(poison-history-tx-order! undo-tx-id))
|
||||
(is (map? (worker-undo-redo/undo test-repo)))
|
||||
(is (empty? (property-value-titles
|
||||
(get (d/entity @conn [:block/uuid child-uuid]) property-id))))
|
||||
(when-let [redo-tx-id (:db-sync/tx-id (latest-redo-history-data))]
|
||||
(poison-history-tx-order! redo-tx-id))
|
||||
(is (map? (worker-undo-redo/redo test-repo)))
|
||||
(let [titles (property-value-titles
|
||||
(get (d/entity @conn [:block/uuid child-uuid]) property-id))]
|
||||
(is (contains? (set titles) "value-1")))))))))
|
||||
|
||||
(deftest save-two-blocks-undo-targets-latest-block-test
|
||||
(testing "undo after saving two blocks reverts the latest saved block first"
|
||||
(worker-undo-redo/clear-history! test-repo)
|
||||
|
||||
Reference in New Issue
Block a user