diff --git a/.clj-kondo/config.edn b/.clj-kondo/config.edn index 5e5707a7d0..22107bd312 100644 --- a/.clj-kondo/config.edn +++ b/.clj-kondo/config.edn @@ -70,6 +70,7 @@ frontend.handler.common.file file-common-handler frontend.handler.common.plugin plugin-common-handler frontend.handler.common.developer dev-common-handler + frontend.handler.common.page page-common-handler frontend.handler.config config-handler frontend.handler.editor.property editor-property frontend.handler.events events @@ -77,9 +78,13 @@ frontend.handler.ui ui-handler frontend.handler.notification notification frontend.handler.page page-handler + frontend.handler.db-based.editor db-editor-handler + frontend.handler.db-based.page db-page-handler frontend.handler.db-based.property db-property-handler + frontend.handler.file-based.page file-page-handler frontend.handler.file-based.property file-property frontend.handler.file-based.property.util property-util + frontend.handler.file-based.recent file-recent-handler frontend.handler.plugin plugin-handler frontend.handler.plugin-config plugin-config-handler frontend.handler.property.util pu diff --git a/src/main/frontend/components/conversion.cljs b/src/main/frontend/components/conversion.cljs index 14bd65c188..dad80cc9d1 100644 --- a/src/main/frontend/components/conversion.cljs +++ b/src/main/frontend/components/conversion.cljs @@ -7,7 +7,7 @@ [frontend.util :as util] [frontend.state :as state] [frontend.ui :as ui] - [frontend.handler.page :as page-handler] + [frontend.handler.file-based.page :as file-page-handler] [frontend.handler.conversion :refer [supported-filename-formats write-filename-format! calc-rename-target]] [frontend.db :as db] [frontend.context.i18n :refer [t]] @@ -138,7 +138,7 @@ c (page-handler/rename-file! file target (constantly nil) true))))) + (async/c (file-page-handler/rename-file! file target (constantly nil) true))))) (file-name path) tgt-file-name (str target "." (gp-util/path->file-ext path)) rm-item-fn #(swap! *pages dissoc path) - rename-fn #(page-handler/rename-file! file target rm-item-fn) + rename-fn #(file-page-handler/rename-file! file target rm-item-fn) rename-but [:a {:on-click rename-fn :title (t :file-rn/apply-rename)} [:span (t :file-rn/rename src-file-name tgt-file-name)]]] diff --git a/src/main/frontend/handler/common/page.cljs b/src/main/frontend/handler/common/page.cljs new file mode 100644 index 0000000000..c135fc733c --- /dev/null +++ b/src/main/frontend/handler/common/page.cljs @@ -0,0 +1,358 @@ +(ns frontend.handler.common.page + "Common fns for file and db based page handlers, including create!, delete! + and favorite fns. This ns should be agnostic of file or db concerns but there + is still some file-specific tech debt to remove from create!" + (:require [frontend.state :as state] + [frontend.config :as config] + [frontend.db :as db] + [frontend.db.model :as model] + [frontend.db.utils :as db-utils] + [frontend.format.block :as block] + [frontend.fs :as fs] + [frontend.handler.common :as common-handler] + [frontend.handler.config :as config-handler] + [frontend.handler.editor :as editor-handler] + [frontend.handler.file-based.editor :as file-editor-handler] + [frontend.handler.route :as route-handler] + [frontend.handler.ui :as ui-handler] + [frontend.util.page-property :as page-property] + [frontend.util.fs :as fs-util] + [frontend.util :as util] + [logseq.db.schema :as db-schema] + [logseq.graph-parser.block :as gp-block] + [logseq.graph-parser.util :as gp-util] + [logseq.graph-parser.text :as text] + [lambdaisland.glogi :as log] + [medley.core :as medley] + [promesa.core :as p] + [clojure.string :as string])) + +;; create! and its helpers +;; ======================= +(defn- build-title [page] + ;; Don't wrap `\"` anymore, as title property is not effected by `,` now + ;; The previous extract behavior isn't unwrapping the `'"` either. So no need + ;; to maintain the compatibility. + (:block/original-name page)) + +(defn- default-properties-block + ([title format page] + (default-properties-block title format page {})) + ([title format page properties] + (let [repo (state/get-current-repo) + db-based? (config/db-based-graph? repo)] + (when-not db-based? + (let [p (common-handler/get-page-default-properties title) + ps (merge p properties) + content (if db-based? + "" + (page-property/insert-properties format "" ps)) + refs (gp-block/get-page-refs-from-properties properties + (db/get-db repo) + (state/get-date-formatter) + (state/get-config))] + {:block/uuid (db/new-block-id) + :block/refs refs + :block/left page + :block/format format + :block/content content + :block/parent page + :block/page page + :block/pre-block? true + :block/properties ps + :block/properties-order (keys ps)}))))) + +(defn- create-title-property? + [repo journal? page-name] + (and (not (config/db-based-graph? repo)) + (not journal?) + (= (state/get-filename-format) :legacy) ;; reduce title computation + (fs-util/create-title-property? page-name))) + +(defn- build-page-tx [repo format properties page journal? {:keys [whiteboard? class? tags]}] + (when (:block/uuid page) + (let [page-entity [:block/uuid (:block/uuid page)] + title (util/get-page-original-name page) + create-title? (create-title-property? repo journal? title) + page (merge page + (when (seq properties) {:block/properties properties}) + (when whiteboard? {:block/type "whiteboard"}) + (when class? {:block/type "class"}) + (when tags {:block/tags (mapv #(hash-map :db/id + (:db/id (db/entity repo [:block/uuid %]))) + tags)})) + page-empty? (db/page-empty? (state/get-current-repo) (:block/name page)) + db-based? (config/db-based-graph? (state/get-current-repo))] + (cond + (not page-empty?) + [page] + + (and create-title? + (not whiteboard?) + (not db-based?)) + (let [properties-block (default-properties-block (build-title page) format page-entity properties)] + [page + properties-block]) + + (and (seq properties) + (not whiteboard?) + (not db-based?)) + [page (file-editor-handler/properties-block repo properties format page-entity)] + + :else + [page])))) + +;; TODO: Move file graph concerns to file-based-handler ns +(defn create! + "Create page. Has the following options: + + * :redirect? - when true, redirect to the created page, otherwise return sanitized page name. + * :split-namespace? - when true, split hierarchical namespace into levels. + * :create-first-block? - when true, create an empty block if the page is empty. + * :uuid - when set, use this uuid instead of generating a new one. + * :class? - when true, adds a :block/type 'class' + * :whiteboard? - when true, adds a :block/type 'whiteboard' + * :tags - tag uuids that are added to :block/tags + * :persist-op? - when true, add an update-page op + TODO: Add other options" + ([title] + (create! title {})) + ([title {:keys [redirect? create-first-block? format properties split-namespace? journal? uuid rename? persist-op?] + :or {redirect? true + create-first-block? true + rename? false + format nil + properties nil + split-namespace? true + uuid nil + persist-op? true} + :as options}] + (let [title (-> (string/trim title) + (text/page-ref-un-brackets!) + ;; remove `#` from tags + (string/replace #"^#+" "")) + title (gp-util/remove-boundary-slashes title) + page-name (util/page-name-sanity-lc title) + repo (state/get-current-repo) + with-uuid? (if (uuid? uuid) uuid true)] ;; FIXME: prettier validation + (when (or (db/page-empty? repo page-name) rename?) + (let [pages (if split-namespace? + (gp-util/split-namespace-pages title) + [title]) + format (or format (state/get-preferred-format)) + pages (map (fn [page] + ;; only apply uuid to the deepest hierarchy of page to create if provided. + (-> (block/page-name->map page (if (= page title) with-uuid? true)) + (assoc :block/format format))) + pages) + txs (->> pages + ;; for namespace pages, only last page need properties + drop-last + (mapcat #(build-page-tx repo format nil % journal? {})) + (remove nil?)) + txs (map-indexed (fn [i page] + (if (zero? i) + page + (assoc page :block/namespace + [:block/uuid (:block/uuid (nth txs (dec i)))]))) + txs) + last-txs (build-page-tx repo format properties (last pages) journal? (select-keys options [:whiteboard? :class? :tags])) + last-txs (if (seq txs) + (update last-txs 0 + (fn [p] + (assoc p :block/namespace [:block/uuid (:block/uuid (last txs))]))) + last-txs) + txs (concat + (when (and rename? uuid) + (when-let [e (db/entity [:block/uuid uuid])] + [[:db/retract (:db/id e) :block/namespace] + [:db/retract (:db/id e) :block/refs]])) + txs + last-txs)] + (when (seq txs) + (db/transact! repo txs {:persist-op? persist-op?}))) + + (when create-first-block? + (when (or + (db/page-empty? repo (:db/id (db/entity [:block/name page-name]))) + (create-title-property? repo journal? page-name)) + (editor-handler/api-insert-new-block! "" {:page page-name})))) + + (when redirect? + (route-handler/redirect-to-page! page-name)) + page-name))) + +;; favorite fns +;; ============ +(defn favorited? + [page-name] + (let [favorites (->> (:favorites (state/get-config)) + (filter string?) + (map string/lower-case) + (set))] + (contains? favorites page-name))) + +(defn favorite-page! + [page-name] + (when-not (string/blank? page-name) + (let [favorites (-> + (cons + page-name + (or (:favorites (state/get-config)) [])) + (distinct) + (vec))] + (config-handler/set-config! :favorites favorites)))) + +(defn unfavorite-page! + [page-name] + (when-not (string/blank? page-name) + (let [old-favorites (:favorites (state/get-config)) + new-favorites (->> old-favorites + (remove #(= (string/lower-case %) (string/lower-case page-name))) + (vec))] + (when-not (= old-favorites new-favorites) + (config-handler/set-config! :favorites new-favorites))))) + +;; delete! and its helpers +;; ======================= +(defn delete-file! + [repo page-name unlink-file?] + (let [file (db/get-page-file page-name) + file-path (:file/path file)] + ;; delete file + (when-not (string/blank? file-path) + (db/transact! [[:db.fn/retractEntity [:file/path file-path]]]) + (when unlink-file? + (-> (fs/unlink! repo (config/get-repo-fpath repo file-path) nil) + (p/catch (fn [error] (js/console.error error)))))))) + +(defn db-refs->page + "Replace [[page name]] with page name" + [repo page-entity] + (when (config/db-based-graph? repo) + (let [refs (:block/_refs page-entity) + id-ref->page #(db-utils/special-id-ref->page % [page-entity])] + (when (seq refs) + (let [tx-data (mapcat (fn [{:block/keys [raw-content properties] :as ref}] + ;; block content or properties + (let [content' (id-ref->page raw-content) + content-tx (when (not= raw-content content') + {:db/id (:db/id ref) + :block/content content'}) + page-uuid (:block/uuid page-entity) + properties' (-> (medley/map-vals (fn [v] + (cond + (and (coll? v) (uuid? (first v))) + (vec (remove #{page-uuid} v)) + + (and (uuid? v) (= v page-uuid)) + nil + + (and (coll? v) (string? (first v))) + (mapv id-ref->page v) + + (string? v) + (id-ref->page v) + + :else + v)) properties) + (util/remove-nils-non-nested)) + tx (merge + content-tx + (when (not= (seq properties) (seq properties')) + {:db/id (:db/id ref) + :block/properties properties'}))] + (concat + [[:db/retract (:db/id ref) :block/refs (:db/id page-entity)]] + (when tx [tx])))) refs)] + tx-data))))) + +(defn- page-unable-to-delete + "If a page is unable to delete, returns a map with more information. Otherwise returns nil" + [repo page] + (try + (cond + (and (contains? (:block/type page) "class") + (seq (model/get-tag-blocks repo (:block/name page)))) + {:msg "Unable to delete this page because blocks are tagged with this page"} + (contains? (:block/type page) "property") + (cond (seq (model/get-classes-with-property (:block/uuid page))) + {:msg "Unable to delete this page because classes use this property"} + (->> (model/get-block-property-values (:block/uuid page)) + (filter (fn [[_ v]] (if (seq? v) (seq v) (some? v)))) + seq) + {:msg "Unable to delete this page because blocks use this property"})) + (catch :default e + (log/error :exception e) + (state/pub-event! [:capture-error {:error e}]) + {:msg (str "An unexpected failure while deleting: " e)}))) + +(defn delete! + "Deletes a page and then either calls the ok-handler or the error-handler if unable to delete" + [page-name ok-handler & {:keys [delete-file? redirect-to-home? persist-op? error-handler] + :or {delete-file? true + redirect-to-home? true + persist-op? true + error-handler (fn [{:keys [msg]}] (log/error :msg msg))}}] + (when redirect-to-home? (route-handler/redirect-to-home!)) + (when page-name + (when-let [repo (state/get-current-repo)] + (let [page-name (util/page-name-sanity-lc page-name) + blocks (db/get-page-blocks-no-cache page-name) + truncate-blocks-tx-data (mapv + (fn [block] + [:db.fn/retractEntity [:block/uuid (:block/uuid block)]]) + blocks) + page (db/entity [:block/name page-name])] + (if-let [msg (and (config/db-based-graph? repo) + (page-unable-to-delete repo page))] + (error-handler msg) + (let [_ (delete-file! repo page-name delete-file?) + ;; if other page alias this pagename, + ;; then just remove some attrs of this entity instead of retractEntity + delete-page-tx (cond + (not (:block/_namespace page)) + (if (model/get-alias-source-page (state/get-current-repo) page-name) + (when-let [id (:db/id (db/entity [:block/name page-name]))] + (mapv (fn [attribute] + [:db/retract id attribute]) + db-schema/retract-page-attributes)) + (concat (db-refs->page repo page) + [[:db.fn/retractEntity [:block/name page-name]]])) + + :else + nil) + tx-data (concat truncate-blocks-tx-data delete-page-tx)] + (db/transact! repo tx-data {:outliner-op :delete-page :persist-op? persist-op?}) + + (unfavorite-page! page-name) + + (when (fn? ok-handler) (ok-handler)) + (ui-handler/re-render-root!))))))) + + +;; other fns +;; ========= +(defn rename-update-namespace! + "update :block/namespace of the renamed block" + [page old-original-name new-name] + (let [old-namespace? (text/namespace-page? old-original-name) + new-namespace? (text/namespace-page? new-name) + repo (state/get-current-repo)] + (cond + new-namespace? + ;; update namespace + (let [namespace (first (gp-util/split-last "/" new-name))] + (when namespace + (create! namespace {:redirect? false}) ;; create parent page if not exist, creation of namespace ref is handled in `create!` + (let [namespace-block (db/pull [:block/name (gp-util/page-name-sanity-lc namespace)]) + page-txs [{:db/id (:db/id page) + :block/namespace (:db/id namespace-block)}]] + (db/transact! repo page-txs)))) + + old-namespace? + ;; retract namespace + (db/transact! [[:db/retract (:db/id page) :block/namespace]]) + + :else + nil))) diff --git a/src/main/frontend/handler/db_based/page.cljs b/src/main/frontend/handler/db_based/page.cljs new file mode 100644 index 0000000000..6b3cc78135 --- /dev/null +++ b/src/main/frontend/handler/db_based/page.cljs @@ -0,0 +1,143 @@ +(ns frontend.handler.db-based.page + "Page handlers for DB graphs" + (:require [frontend.state :as state] + [frontend.db :as db] + [frontend.db.model :as model] + [frontend.db.conn :as conn] + [frontend.db.utils :as db-utils] + [frontend.util :as util] + [frontend.handler.ui :as ui-handler] + [frontend.handler.notification :as notification] + [frontend.handler.route :as route-handler] + [frontend.modules.outliner.core :as outliner-core] + [frontend.modules.outliner.tree :as outliner-tree] + [frontend.handler.common.page :as page-common-handler] + [datascript.core :as d] + [medley.core :as medley] + [clojure.string :as string])) + +(defn- replace-ref + "Replace from-page refs with to-page" + [from-page to-page] + (let [refs (:block/_refs from-page) + from-uuid (:block/uuid from-page) + to-uuid (:block/uuid to-page) + replace-ref (fn [content] (string/replace content (str from-uuid) (str to-uuid)))] + (when (seq refs) + (let [tx-data (mapcat + (fn [{:block/keys [raw-content properties] :as ref}] + ;; block content or properties + (let [content' (replace-ref raw-content) + content-tx (when (not= raw-content content') + {:db/id (:db/id ref) + :block/content content'}) + properties' (-> (medley/map-vals (fn [v] + (cond + (and (coll? v) (uuid? (first v))) + (mapv (fn [id] (if (= id from-uuid) to-uuid id)) v) + + (and (uuid? v) (= v from-uuid)) + to-uuid + + (and (coll? v) (string? (first v))) + (mapv replace-ref v) + + (string? v) + (replace-ref v) + + :else + v)) properties) + (util/remove-nils-non-nested)) + tx (merge + content-tx + (when (not= (seq properties) (seq properties')) + {:db/id (:db/id ref) + :block/properties properties'}))] + (concat + [[:db/add (:db/id ref) :block/refs (:db/id to-page)] + [:db/retract (:db/id ref) :block/refs (:db/id from-page)]] + (when tx [tx])))) + refs)] + tx-data)))) + +(defn based-merge-pages! + [from-page-name to-page-name persist-op?] + (when (and (db/page-exists? from-page-name) + (db/page-exists? to-page-name) + (not= from-page-name to-page-name)) + (let [to-page (db/entity [:block/name to-page-name]) + to-id (:db/id to-page) + from-page (db/entity [:block/name from-page-name]) + from-id (:db/id from-page) + from-first-child (some->> (db/pull from-id) + (outliner-core/block) + (outliner-tree/-get-down) + (outliner-core/get-data)) + to-last-direct-child-id (model/get-block-last-direct-child (db/get-db) to-id false) + repo (state/get-current-repo) + conn (conn/get-db repo false) + datoms (d/datoms @conn :avet :block/page from-id) + block-eids (mapv :e datoms) + blocks (db-utils/pull-many repo '[:db/id :block/page :block/refs :block/path-refs :block/left :block/parent] block-eids) + blocks-tx-data (map (fn [block] + (let [id (:db/id block)] + (cond-> + {:db/id id + :block/page {:db/id to-id}} + + (and from-first-child (= id (:db/id from-first-child))) + (assoc :block/left {:db/id (or to-last-direct-child-id to-id)}) + + (= (:block/parent block) {:db/id from-id}) + (assoc :block/parent {:db/id to-id})))) blocks) + replace-ref-tx-data (replace-ref from-page to-page) + tx-data (concat blocks-tx-data replace-ref-tx-data)] + (db/transact! repo tx-data {:persist-op? persist-op?}) + (page-common-handler/rename-update-namespace! from-page + (util/get-page-original-name from-page) + (util/get-page-original-name to-page))) + + + (page-common-handler/delete! from-page-name nil :redirect-to-home? false :persist-op? persist-op?) + + (route-handler/redirect! {:to :page + :push false + :path-params {:name to-page-name}}))) + +(defn rename! + ([old-name new-name] + (rename! old-name new-name true true)) + ([old-name new-name redirect? persist-op?] + (let [repo (state/get-current-repo) + old-name (string/trim old-name) + new-name (string/trim new-name) + old-page-name (util/page-name-sanity-lc old-name) + page-e (db/entity [:block/name old-page-name]) + new-page-name (util/page-name-sanity-lc new-name) + name-changed? (not= old-name new-name)] + (if (and old-name + new-name + (not (string/blank? new-name)) + name-changed?) + (cond + (= old-page-name new-page-name) ; case changed + (db/transact! repo + [{:db/id (:db/id page-e) + :block/original-name new-name}] + {:persist-op? persist-op?}) + + (and (not= old-page-name new-page-name) + (db/entity [:block/name new-page-name])) ; merge page + (based-merge-pages! old-page-name new-page-name persist-op?) + + :else ; rename + (page-common-handler/create! new-name + {:rename? true + :uuid (:block/uuid page-e) + :redirect? redirect? + :create-first-block? false + :persist-op? persist-op?})) + + (when (string/blank? new-name) + (notification/show! "Please use a valid name, empty name is not allowed!" :error))) + (ui-handler/re-render-root!)))) \ No newline at end of file diff --git a/src/main/frontend/handler/file_based/page.cljs b/src/main/frontend/handler/file_based/page.cljs new file mode 100644 index 0000000000..5af6b0ac80 --- /dev/null +++ b/src/main/frontend/handler/file_based/page.cljs @@ -0,0 +1,389 @@ +(ns frontend.handler.file-based.page + "Page handlers for file based graphs" + (:require [frontend.config :as config] + [frontend.db :as db] + [frontend.db.conn :as conn] + [frontend.db.utils :as db-utils] + [frontend.db.model :as model] + [frontend.handler.file-based.property :as file-property] + [frontend.handler.file-based.recent :as file-recent-handler] + [frontend.handler.config :as config-handler] + [frontend.handler.common.page :as page-common-handler] + [frontend.handler.notification :as notification] + [frontend.handler.route :as route-handler] + [frontend.handler.ui :as ui-handler] + [frontend.state :as state] + [frontend.util :as util] + [frontend.util.fs :as fs-util] + [frontend.util.page-property :as page-property] + [frontend.modules.outliner.core :as outliner-core] + [frontend.modules.outliner.file :as outliner-file] + [frontend.modules.outliner.tree :as outliner-tree] + [frontend.fs :as fs] + [logseq.graph-parser.property :as gp-property] + [logseq.graph-parser.util.page-ref :as page-ref] + [lambdaisland.glogi :as log] + [promesa.core :as p] + [datascript.core :as d] + [clojure.walk :as walk] + [clojure.string :as string])) + +(defn- replace-page-ref! + "Unsanitized names" + [content old-name new-name] + (let [[original-old-name original-new-name] (map string/trim [old-name new-name]) + [old-ref new-ref] (map page-ref/->page-ref [old-name new-name]) + [old-name new-name] (map #(if (string/includes? % "/") + (string/replace % "/" ".") + %) + [original-old-name original-new-name]) + old-org-ref (and (= :org (state/get-preferred-format)) + (:org-mode/insert-file-link? (state/get-config)) + (re-find + (re-pattern + (util/format + "\\[\\[file:\\.*/.*%s\\.org\\]\\[(.*?)\\]\\]" old-name)) + content))] + (-> (if old-org-ref + (let [[old-full-ref old-label] old-org-ref + new-label (if (= old-label original-old-name) + original-new-name + old-label) + new-full-ref (-> (string/replace old-full-ref old-name new-name) + (string/replace (str "[" old-label "]") + (str "[" new-label "]")))] + (string/replace content old-full-ref new-full-ref)) + content) + (string/replace old-ref new-ref)))) + +(defn- replace-tag-ref! + [content old-name new-name] + (let [old-tag (util/format "#%s" old-name) + new-tag (if (re-find #"[\s\t]+" new-name) + (util/format "#[[%s]]" new-name) + (str "#" new-name))] + ;; hash tag parsing rules https://github.com/logseq/mldoc/blob/701243eaf9b4157348f235670718f6ad19ebe7f8/test/test_markdown.ml#L631 + ;; Safari doesn't support look behind, don't use + ;; TODO: parse via mldoc + (string/replace content + (re-pattern (str "(?i)(^|\\s)(" (util/escape-regex-chars old-tag) ")(?=[,\\.]*($|\\s))")) + ;; case_insense^ ^lhs ^_grp2 look_ahead^ ^_grp3 + (fn [[_match lhs _grp2 _grp3]] + (str lhs new-tag))))) + +(defn- replace-property-ref! + [content old-name new-name format] + (let [new-name (keyword (string/replace (string/lower-case new-name) #"\s+" "-")) + org-format? (= :org format) + old-property (if org-format? (gp-property/colons-org old-name) (str old-name gp-property/colons)) + new-property (if org-format? (gp-property/colons-org (name new-name)) (str (name new-name) gp-property/colons))] + (util/replace-ignore-case content old-property new-property))) + +(defn- replace-old-page! + "Unsanitized names" + [content old-name new-name format] + (when (and (string? content) (string? old-name) (string? new-name)) + (-> content + (replace-page-ref! old-name new-name) + (replace-tag-ref! old-name new-name) + (replace-property-ref! old-name new-name format)))) + +(defn- walk-replace-old-page! + "Unsanitized names" + [form old-name new-name format] + (walk/postwalk (fn [f] + (cond + (and (vector? f) + (contains? #{"Search" "Label"} (first f)) + (string/starts-with? (second f) (str old-name "/"))) + [(first f) (string/replace-first (second f) + (str old-name "/") + (str new-name "/"))] + + (string? f) + (if (= f old-name) + new-name + (replace-old-page! f old-name new-name format)) + + (and (keyword f) (= (name f) old-name)) + (keyword (string/replace (string/lower-case new-name) #"\s+" "-")) + + :else + f)) + form)) + +(defn- rename-update-block-refs! + [refs from-id to-id] + (->> refs + (remove #{{:db/id from-id}}) + (cons {:db/id to-id}) + (distinct) + (vec))) + +(defn- rename-update-refs! + "Unsanitized only" + [page old-original-name new-name] + ;; update all pages which have references to this page + (let [repo (state/get-current-repo) + to-page (db/entity [:block/name (util/page-name-sanity-lc new-name)]) + blocks (:block/_refs (db/entity (:db/id page))) + page-ids (->> (map (fn [b] + {:db/id (:db/id (:block/page b))}) blocks) + (set)) + tx (->> (map (fn [{:block/keys [uuid content properties format] :as block}] + (let [content (let [content' (replace-old-page! content old-original-name new-name format)] + (when-not (= content' content) + content')) + properties (let [properties' (walk-replace-old-page! properties old-original-name new-name format)] + (when-not (= properties' properties) + properties'))] + (when (or content properties) + (util/remove-nils-non-nested + {:block/uuid uuid + :block/content content + :block/properties properties + :block/properties-order (when (seq properties) + (map first properties)) + :block/refs (->> (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page)) + (map :db/id) + (set))})))) blocks) + (remove nil?))] + (db/transact! repo tx) + (doseq [page-id page-ids] + (outliner-file/sync-to-file page-id)))) + +(defn- compute-new-file-path + "Construct the full path given old full path and the file sanitized body. + Ext. included in the `old-path`." + [old-path new-file-name-body] + (let [result (string/split old-path "/") + ext (last (string/split (last result) ".")) + new-file (str new-file-name-body "." ext) + parts (concat (butlast result) [new-file])] + (util/string-join-path parts))) + +(defn rename-file! + "emit file-rename events to :file/rename-event-chan + force-fs? - when true, rename file event the db transact is failed." + ([file new-file-name-body ok-handler] + (rename-file! file new-file-name-body ok-handler false)) + ([file new-file-name-body ok-handler force-fs?] + (let [repo (state/get-current-repo) + file (db/pull (:db/id file)) + old-path (:file/path file) + new-path (compute-new-file-path old-path new-file-name-body) + transact #(db/transact! repo [{:db/id (:db/id file) + :file/path new-path}])] + ;; update db + (if force-fs? + (try (transact) ;; capture error and continue FS rename if failed + (catch :default e + (log/error :rename-file e))) + (transact)) ;; interrupted if failed + + (-> + (p/let [_ (state/offer-file-rename-event-chan! {:repo repo + :old-path old-path + :new-path new-path}) + _ (fs/rename! repo old-path new-path)] + (ok-handler)) + (p/catch (fn [error] + (println "file rename failed: " error))))))) + +(defn- rename-page-aux + "Only accepts unsanitized page names" + [old-name new-name redirect?] + (let [old-page-name (util/page-name-sanity-lc old-name) + new-file-name-body (fs-util/file-name-sanity new-name) ;; w/o file extension + new-page-name (util/page-name-sanity-lc new-name) + repo (state/get-current-repo) + page (db/pull [:block/name old-page-name])] + (when (and repo page) + (let [old-original-name (:block/original-name page) + file (:block/file page) + journal? (:block/journal? page) + properties-block (:data (outliner-tree/-get-down (outliner-core/block page))) + properties-content (:block/content properties-block) + properties-block-tx (when (and properties-block + properties-content + (string/includes? (util/page-name-sanity-lc properties-content) + old-page-name)) + (let [front-matter? (and (file-property/front-matter?-when-file-based properties-content) + (= :markdown (:block/format properties-block)))] + {:db/id (:db/id properties-block) + :block/content (file-property/insert-property + (:block/format properties-block) + properties-content + :title + new-name + front-matter?)})) + page-txs [{:db/id (:db/id page) + :block/uuid (:block/uuid page) + :block/name new-page-name + :block/original-name new-name}] + page-txs (if properties-block-tx (conj page-txs properties-block-tx) page-txs)] + + (db/transact! repo page-txs) + + (when (and (not (config/db-based-graph? repo)) + (fs-util/create-title-property? new-page-name)) + (page-property/add-property! new-page-name :title new-name)) + + (when (and file (not journal?)) + (rename-file! file new-file-name-body (fn [] nil))) + + (let [home (get (state/get-config) :default-home {})] + (when (= old-page-name (util/page-name-sanity-lc (get home :page ""))) + (config-handler/set-config! :default-home (assoc home :page new-name)))) + + (rename-update-refs! page old-original-name new-name) + + (page-common-handler/rename-update-namespace! page old-original-name new-name) + + (outliner-file/sync-to-file page)) + + ;; Redirect to the newly renamed page + (when redirect? + (route-handler/redirect! {:to (if (model/whiteboard-page? page) :whiteboard :page) + :push false + :path-params {:name new-page-name}})) + + (when (page-common-handler/favorited? old-page-name) + (p/do! + (page-common-handler/unfavorite-page! old-page-name) + (page-common-handler/favorite-page! new-page-name))) + + (file-recent-handler/update-or-add-renamed-page repo old-page-name new-page-name) + + (ui-handler/re-render-root!)))) + +(defn- rename-nested-pages + "Unsanitized names only" + [old-ns-name new-ns-name] + (let [repo (state/get-current-repo) + nested-page-str (page-ref/->page-ref (util/page-name-sanity-lc old-ns-name)) + ns-prefix-format-str (str page-ref/left-brackets "%s/") + ns-prefix (util/format ns-prefix-format-str (util/page-name-sanity-lc old-ns-name)) + nested-pages (db/get-pages-by-name-partition repo nested-page-str) + nested-pages-ns (db/get-pages-by-name-partition repo ns-prefix)] + (when nested-pages + ;; rename page "[[obsidian]] is a tool" to "[[logseq]] is a tool" + (doseq [{:block/keys [name original-name]} nested-pages] + (let [old-page-title (or original-name name) + new-page-title (string/replace + old-page-title + (page-ref/->page-ref old-ns-name) + (page-ref/->page-ref new-ns-name))] + (when (and old-page-title new-page-title) + (p/do! + (rename-page-aux old-page-title new-page-title false) + (println "Renamed " old-page-title " to " new-page-title)))))) + (when nested-pages-ns + ;; rename page "[[obsidian/page1]] is a tool" to "[[logseq/page1]] is a tool" + (doseq [{:block/keys [name original-name]} nested-pages-ns] + (let [old-page-title (or original-name name) + new-page-title (string/replace + old-page-title + (util/format ns-prefix-format-str old-ns-name) + (util/format ns-prefix-format-str new-ns-name))] + (when (and old-page-title new-page-title) + (p/do! + (rename-page-aux old-page-title new-page-title false) + (println "Renamed " old-page-title " to " new-page-title)))))))) + +(defn- rename-namespace-pages! + "Original names (unsanitized only)" + [repo old-name new-name] + (let [pages (db/get-namespace-pages repo old-name) + page (db/pull [:block/name (util/page-name-sanity-lc old-name)]) + pages (cons page pages)] + (doseq [{:block/keys [name original-name]} pages] + (let [old-page-title (or original-name name) + ;; only replace one time, for the case that the namespace is a sub-string of the sub-namespace page name + ;; Example: has pages [[work]] [[work/worklog]], + ;; we want to rename [[work/worklog]] to [[work1/worklog]] when rename [[work]] to [[work1]], + ;; but don't rename [[work/worklog]] to [[work1/work1log]] + new-page-title (string/replace-first old-page-title old-name new-name) + redirect? (= name (:block/name page))] + (when (and old-page-title new-page-title) + (p/let [_ (rename-page-aux old-page-title new-page-title redirect?)] + (println "Renamed " old-page-title " to " new-page-title))))))) + +(defn merge-pages! + "Only accepts sanitized page names" + [from-page-name to-page-name] + (when (and (db/page-exists? from-page-name) + (db/page-exists? to-page-name) + (not= from-page-name to-page-name)) + (let [to-page (db/entity [:block/name to-page-name]) + to-id (:db/id to-page) + from-page (db/entity [:block/name from-page-name]) + from-id (:db/id from-page) + from-first-child (some->> (db/pull from-id) + (outliner-core/block) + (outliner-tree/-get-down) + (outliner-core/get-data)) + to-last-direct-child-id (model/get-block-last-direct-child (db/get-db) to-id false) + repo (state/get-current-repo) + conn (conn/get-db repo false) + datoms (d/datoms @conn :avet :block/page from-id) + block-eids (mapv :e datoms) + blocks (db-utils/pull-many repo '[:db/id :block/page :block/refs :block/path-refs :block/left :block/parent] block-eids) + tx-data (map (fn [block] + (let [id (:db/id block)] + (cond-> + {:db/id id + :block/page {:db/id to-id} + :block/refs (rename-update-block-refs! (:block/refs block) from-id to-id)} + + (and from-first-child (= id (:db/id from-first-child))) + (assoc :block/left {:db/id (or to-last-direct-child-id to-id)}) + + (= (:block/parent block) {:db/id from-id}) + (assoc :block/parent {:db/id to-id})))) blocks)] + (db/transact! repo tx-data) + (outliner-file/sync-to-file {:db/id to-id}) + + (rename-update-refs! from-page + (util/get-page-original-name from-page) + (util/get-page-original-name to-page)) + + (page-common-handler/rename-update-namespace! from-page + (util/get-page-original-name from-page) + (util/get-page-original-name to-page))) + + + (page-common-handler/delete! from-page-name nil) + + (route-handler/redirect! {:to :page + :push false + :path-params {:name to-page-name}}))) + +(defn rename! + "Accepts unsanitized page names" + ([old-name new-name] (rename! old-name new-name true)) + ([old-name new-name redirect?] + (let [repo (state/get-current-repo) + old-name (string/trim old-name) + new-name (string/trim new-name) + old-page-name (util/page-name-sanity-lc old-name) + new-page-name (util/page-name-sanity-lc new-name) + name-changed? (not= old-name new-name)] + (if (and old-name + new-name + (not (string/blank? new-name)) + name-changed?) + (do + (cond + (= old-page-name new-page-name) + (rename-page-aux old-name new-name redirect?) + + (db/pull [:block/name new-page-name]) + (merge-pages! old-page-name new-page-name) + + :else + (rename-namespace-pages! repo old-name new-name)) + (rename-nested-pages old-name new-name)) + (when (string/blank? new-name) + (notification/show! "Please use a valid name, empty name is not allowed!" :error))) + (ui-handler/re-render-root!)))) \ No newline at end of file diff --git a/src/main/frontend/handler/page.cljs b/src/main/frontend/handler/page.cljs index aa0347d479..3beda56a1d 100644 --- a/src/main/frontend/handler/page.cljs +++ b/src/main/frontend/handler/page.cljs @@ -2,56 +2,46 @@ "Provides util handler fns for pages" (:require [cljs.reader :as reader] [clojure.string :as string] - [clojure.walk :as walk] - [datascript.core :as d] [frontend.commands :as commands] [frontend.config :as config] [frontend.date :as date] [frontend.db :as db] - [frontend.db.conn :as conn] [frontend.db.model :as model] - [frontend.db.utils :as db-utils] - [frontend.format.block :as block] [frontend.fs :as fs] [frontend.handler.common :as common-handler] + [frontend.handler.common.page :as page-common-handler] [frontend.handler.reorder :as reorder-handler] [frontend.handler.config :as config-handler] [frontend.handler.editor :as editor-handler] - [frontend.handler.file-based.editor :as file-editor-handler] [frontend.handler.plugin :as plugin-handler] [frontend.handler.notification :as notification] - [frontend.handler.file-based.recent :as recent-handler] - [frontend.handler.route :as route-handler] + [frontend.handler.db-based.page :as db-page-handler] + [frontend.handler.file-based.page :as file-page-handler] [frontend.handler.ui :as ui-handler] [frontend.handler.web.nfs :as web-nfs] [frontend.mobile.util :as mobile-util] - [frontend.modules.outliner.core :as outliner-core] - [frontend.modules.outliner.file :as outliner-file] - [frontend.modules.outliner.tree :as outliner-tree] [frontend.state :as state] [frontend.util :as util] [frontend.util.cursor :as cursor] - [frontend.util.fs :as fs-util] [frontend.util.page-property :as page-property] [frontend.util.page :as page-util] - [frontend.handler.file-based.property :as file-property] [frontend.util.url :as url-util] [goog.functions :refer [debounce]] [goog.object :as gobj] [lambdaisland.glogi :as log] - [logseq.db.schema :as db-schema] [logseq.db.property :as db-property] - [logseq.graph-parser.block :as gp-block] [logseq.graph-parser.config :as gp-config] - [logseq.graph-parser.property :as gp-property] - [logseq.graph-parser.text :as text] [logseq.graph-parser.util :as gp-util] [logseq.graph-parser.util.page-ref :as page-ref] [promesa.core :as p] [logseq.common.path :as path] - [medley.core :as medley] [frontend.handler.property.util :as pu])) +(def create! page-common-handler/create!) +(def delete! page-common-handler/delete!) +(def unfavorite-page! page-common-handler/unfavorite-page!) +(def favorite-page! page-common-handler/favorite-page!) + ;; FIXME: add whiteboard (defn- get-directory [journal?] @@ -68,320 +58,6 @@ ;; Win10 file path has a length limit of 260 chars (gp-util/safe-subs s 0 200))) -(defn- build-title [page] - ;; Don't wrap `\"` anymore, as title property is not effected by `,` now - ;; The previous extract behavior isn't unwrapping the `'"` either. So no need - ;; to maintain the compatibility. - (:block/original-name page)) - -(defn default-properties-block - ([title format page] - (default-properties-block title format page {})) - ([title format page properties] - (let [repo (state/get-current-repo) - db-based? (config/db-based-graph? repo)] - (when-not db-based? - (let [p (common-handler/get-page-default-properties title) - ps (merge p properties) - content (if db-based? - "" - (page-property/insert-properties format "" ps)) - refs (gp-block/get-page-refs-from-properties properties - (db/get-db repo) - (state/get-date-formatter) - (state/get-config))] - {:block/uuid (db/new-block-id) - :block/refs refs - :block/left page - :block/format format - :block/content content - :block/parent page - :block/page page - :block/pre-block? true - :block/properties ps - :block/properties-order (keys ps)}))))) - -(defn- create-title-property? - [repo journal? page-name] - (and (not (config/db-based-graph? repo)) - (not journal?) - (= (state/get-filename-format) :legacy) ;; reduce title computation - (fs-util/create-title-property? page-name))) - -(defn- build-page-tx [repo format properties page journal? {:keys [whiteboard? class? tags]}] - (when (:block/uuid page) - (let [page-entity [:block/uuid (:block/uuid page)] - title (util/get-page-original-name page) - create-title? (create-title-property? repo journal? title) - page (merge page - (when (seq properties) {:block/properties properties}) - (when whiteboard? {:block/type "whiteboard"}) - (when class? {:block/type "class"}) - (when tags {:block/tags (mapv #(hash-map :db/id - (:db/id (db/entity repo [:block/uuid %]))) - tags)})) - page-empty? (db/page-empty? (state/get-current-repo) (:block/name page)) - db-based? (config/db-based-graph? (state/get-current-repo))] - (cond - (not page-empty?) - [page] - - (and create-title? - (not whiteboard?) - (not db-based?)) - (let [properties-block (default-properties-block (build-title page) format page-entity properties)] - [page - properties-block]) - - (and (seq properties) - (not whiteboard?) - (not db-based?)) - [page (file-editor-handler/properties-block repo properties format page-entity)] - - :else - [page])))) - -(defn create! - "Create page. Has the following options: - - * :redirect? - when true, redirect to the created page, otherwise return sanitized page name. - * :split-namespace? - when true, split hierarchical namespace into levels. - * :create-first-block? - when true, create an empty block if the page is empty. - * :uuid - when set, use this uuid instead of generating a new one. - * :class? - when true, adds a :block/type 'class' - * :whiteboard? - when true, adds a :block/type 'whiteboard' - * :tags - tag uuids that are added to :block/tags - * :persist-op? - when true, add an update-page op - TODO: Add other options" - ([title] - (create! title {})) - ([title {:keys [redirect? create-first-block? format properties split-namespace? journal? uuid rename? persist-op?] - :or {redirect? true - create-first-block? true - rename? false - format nil - properties nil - split-namespace? true - uuid nil - persist-op? true} - :as options}] - (let [title (-> (string/trim title) - (text/page-ref-un-brackets!) - ;; remove `#` from tags - (string/replace #"^#+" "")) - title (gp-util/remove-boundary-slashes title) - page-name (util/page-name-sanity-lc title) - repo (state/get-current-repo) - with-uuid? (if (uuid? uuid) uuid true)] ;; FIXME: prettier validation - (when (or (db/page-empty? repo page-name) rename?) - (let [pages (if split-namespace? - (gp-util/split-namespace-pages title) - [title]) - format (or format (state/get-preferred-format)) - pages (map (fn [page] - ;; only apply uuid to the deepest hierarchy of page to create if provided. - (-> (block/page-name->map page (if (= page title) with-uuid? true)) - (assoc :block/format format))) - pages) - txs (->> pages - ;; for namespace pages, only last page need properties - drop-last - (mapcat #(build-page-tx repo format nil % journal? {})) - (remove nil?)) - txs (map-indexed (fn [i page] - (if (zero? i) - page - (assoc page :block/namespace - [:block/uuid (:block/uuid (nth txs (dec i)))]))) - txs) - last-txs (build-page-tx repo format properties (last pages) journal? (select-keys options [:whiteboard? :class? :tags])) - last-txs (if (seq txs) - (update last-txs 0 - (fn [p] - (assoc p :block/namespace [:block/uuid (:block/uuid (last txs))]))) - last-txs) - txs (concat - (when (and rename? uuid) - (when-let [e (db/entity [:block/uuid uuid])] - [[:db/retract (:db/id e) :block/namespace] - [:db/retract (:db/id e) :block/refs]])) - txs - last-txs)] - (when (seq txs) - (db/transact! repo txs {:persist-op? persist-op?}))) - - (when create-first-block? - (when (or - (db/page-empty? repo (:db/id (db/entity [:block/name page-name]))) - (create-title-property? repo journal? page-name)) - (editor-handler/api-insert-new-block! "" {:page page-name})))) - - (when redirect? - (route-handler/redirect-to-page! page-name)) - page-name))) - -(defn delete-file! - [repo page-name unlink-file?] - (let [file (db/get-page-file page-name) - file-path (:file/path file)] - ;; delete file - (when-not (string/blank? file-path) - (db/transact! [[:db.fn/retractEntity [:file/path file-path]]]) - (when unlink-file? - (-> (fs/unlink! repo (config/get-repo-fpath repo file-path) nil) - (p/catch (fn [error] (js/console.error error)))))))) - -(defn- compute-new-file-path - "Construct the full path given old full path and the file sanitized body. - Ext. included in the `old-path`." - [old-path new-file-name-body] - (let [result (string/split old-path "/") - ext (last (string/split (last result) ".")) - new-file (str new-file-name-body "." ext) - parts (concat (butlast result) [new-file])] - (util/string-join-path parts))) - -(defn rename-file! - "emit file-rename events to :file/rename-event-chan - force-fs? - when true, rename file event the db transact is failed." - ([file new-file-name-body ok-handler] - (rename-file! file new-file-name-body ok-handler false)) - ([file new-file-name-body ok-handler force-fs?] - (let [repo (state/get-current-repo) - file (db/pull (:db/id file)) - old-path (:file/path file) - new-path (compute-new-file-path old-path new-file-name-body) - transact #(db/transact! repo [{:db/id (:db/id file) - :file/path new-path}])] - ;; update db - (if force-fs? - (try (transact) ;; capture error and continue FS rename if failed - (catch :default e - (log/error :rename-file e))) - (transact)) ;; interrupted if failed - - (-> - (p/let [_ (state/offer-file-rename-event-chan! {:repo repo - :old-path old-path - :new-path new-path}) - _ (fs/rename! repo old-path new-path)] - (ok-handler)) - (p/catch (fn [error] - (println "file rename failed: " error))))))) - -(defn- replace-page-ref! - "Unsanitized names" - [content old-name new-name] - (let [[original-old-name original-new-name] (map string/trim [old-name new-name]) - [old-ref new-ref] (map page-ref/->page-ref [old-name new-name]) - [old-name new-name] (map #(if (string/includes? % "/") - (string/replace % "/" ".") - %) - [original-old-name original-new-name]) - old-org-ref (and (= :org (state/get-preferred-format)) - (:org-mode/insert-file-link? (state/get-config)) - (re-find - (re-pattern - (util/format - "\\[\\[file:\\.*/.*%s\\.org\\]\\[(.*?)\\]\\]" old-name)) - content))] - (-> (if old-org-ref - (let [[old-full-ref old-label] old-org-ref - new-label (if (= old-label original-old-name) - original-new-name - old-label) - new-full-ref (-> (string/replace old-full-ref old-name new-name) - (string/replace (str "[" old-label "]") - (str "[" new-label "]")))] - (string/replace content old-full-ref new-full-ref)) - content) - (string/replace old-ref new-ref)))) - -(defn- replace-tag-ref! - [content old-name new-name] - (let [old-tag (util/format "#%s" old-name) - new-tag (if (re-find #"[\s\t]+" new-name) - (util/format "#[[%s]]" new-name) - (str "#" new-name))] - ;; hash tag parsing rules https://github.com/logseq/mldoc/blob/701243eaf9b4157348f235670718f6ad19ebe7f8/test/test_markdown.ml#L631 - ;; Safari doesn't support look behind, don't use - ;; TODO: parse via mldoc - (string/replace content - (re-pattern (str "(?i)(^|\\s)(" (util/escape-regex-chars old-tag) ")(?=[,\\.]*($|\\s))")) - ;; case_insense^ ^lhs ^_grp2 look_ahead^ ^_grp3 - (fn [[_match lhs _grp2 _grp3]] - (str lhs new-tag))))) - -(defn- replace-property-ref! - [content old-name new-name format] - (let [new-name (keyword (string/replace (string/lower-case new-name) #"\s+" "-")) - org-format? (= :org format) - old-property (if org-format? (gp-property/colons-org old-name) (str old-name gp-property/colons)) - new-property (if org-format? (gp-property/colons-org (name new-name)) (str (name new-name) gp-property/colons))] - (util/replace-ignore-case content old-property new-property))) - -(defn- replace-old-page! - "Unsanitized names" - [content old-name new-name format] - (when (and (string? content) (string? old-name) (string? new-name)) - (-> content - (replace-page-ref! old-name new-name) - (replace-tag-ref! old-name new-name) - (replace-property-ref! old-name new-name format)))) - -(defn- walk-replace-old-page! - "Unsanitized names" - [form old-name new-name format] - (walk/postwalk (fn [f] - (cond - (and (vector? f) - (contains? #{"Search" "Label"} (first f)) - (string/starts-with? (second f) (str old-name "/"))) - [(first f) (string/replace-first (second f) - (str old-name "/") - (str new-name "/"))] - - (string? f) - (if (= f old-name) - new-name - (replace-old-page! f old-name new-name format)) - - (and (keyword f) (= (name f) old-name)) - (keyword (string/replace (string/lower-case new-name) #"\s+" "-")) - - :else - f)) - form)) - -(defn favorited? - [page-name] - (let [favorites (->> (:favorites (state/get-config)) - (filter string?) - (map string/lower-case) - (set))] - (contains? favorites page-name))) - -(defn favorite-page! - [page-name] - (when-not (string/blank? page-name) - (let [favorites (-> - (cons - page-name - (or (:favorites (state/get-config)) [])) - (distinct) - (vec))] - (config-handler/set-config! :favorites favorites)))) - -(defn unfavorite-page! - [page-name] - (when-not (string/blank? page-name) - (let [old-favorites (:favorites (state/get-config)) - new-favorites (->> old-favorites - (remove #(= (string/lower-case %) (string/lower-case page-name))) - (vec))] - (when-not (= old-favorites new-favorites) - (config-handler/set-config! :favorites new-favorites))))) - (defn toggle-favorite! [] ;; NOTE: in journals or settings, current-page is nil (when-let [page-name (state/get-current-page)] @@ -392,506 +68,13 @@ (unfavorite-page! page-name) (favorite-page! page-name))))) -(defn db-refs->page - "Replace [[page name]] with page name" - [repo page-entity] - (when (config/db-based-graph? repo) - (let [refs (:block/_refs page-entity) - id-ref->page #(db-utils/special-id-ref->page % [page-entity])] - (when (seq refs) - (let [tx-data (mapcat (fn [{:block/keys [raw-content properties] :as ref}] - ;; block content or properties - (let [content' (id-ref->page raw-content) - content-tx (when (not= raw-content content') - {:db/id (:db/id ref) - :block/content content'}) - page-uuid (:block/uuid page-entity) - properties' (-> (medley/map-vals (fn [v] - (cond - (and (coll? v) (uuid? (first v))) - (vec (remove #{page-uuid} v)) - - (and (uuid? v) (= v page-uuid)) - nil - - (and (coll? v) (string? (first v))) - (mapv id-ref->page v) - - (string? v) - (id-ref->page v) - - :else - v)) properties) - (util/remove-nils-non-nested)) - tx (merge - content-tx - (when (not= (seq properties) (seq properties')) - {:db/id (:db/id ref) - :block/properties properties'}))] - (concat - [[:db/retract (:db/id ref) :block/refs (:db/id page-entity)]] - (when tx [tx])))) refs)] - tx-data))))) - -(defn db-replace-ref - "Replace from-page refs with to-page" - [repo from-page to-page] - (when (config/db-based-graph? repo) - (let [refs (:block/_refs from-page) - from-uuid (:block/uuid from-page) - to-uuid (:block/uuid to-page) - replace-ref (fn [content] (string/replace content (str from-uuid) (str to-uuid)))] - (when (seq refs) - (let [tx-data (mapcat - (fn [{:block/keys [raw-content properties] :as ref}] - ;; block content or properties - (let [content' (replace-ref raw-content) - content-tx (when (not= raw-content content') - {:db/id (:db/id ref) - :block/content content'}) - properties' (-> (medley/map-vals (fn [v] - (cond - (and (coll? v) (uuid? (first v))) - (mapv (fn [id] (if (= id from-uuid) to-uuid id)) v) - - (and (uuid? v) (= v from-uuid)) - to-uuid - - (and (coll? v) (string? (first v))) - (mapv replace-ref v) - - (string? v) - (replace-ref v) - - :else - v)) properties) - (util/remove-nils-non-nested)) - tx (merge - content-tx - (when (not= (seq properties) (seq properties')) - {:db/id (:db/id ref) - :block/properties properties'}))] - (concat - [[:db/add (:db/id ref) :block/refs (:db/id to-page)] - [:db/retract (:db/id ref) :block/refs (:db/id from-page)]] - (when tx [tx])))) - refs)] - tx-data))))) - -(defn- page-unable-to-delete - "If a page is unable to delete, returns a map with more information. Otherwise returns nil" - [repo page] - (try - (cond - (and (contains? (:block/type page) "class") - (seq (model/get-tag-blocks repo (:block/name page)))) - {:msg "Unable to delete this page because blocks are tagged with this page"} - (contains? (:block/type page) "property") - (cond (seq (model/get-classes-with-property (:block/uuid page))) - {:msg "Unable to delete this page because classes use this property"} - (->> (model/get-block-property-values (:block/uuid page)) - (filter (fn [[_ v]] (if (seq? v) (seq v) (some? v)))) - seq) - {:msg "Unable to delete this page because blocks use this property"})) - (catch :default e - (log/error :exception e) - (state/pub-event! [:capture-error {:error e}]) - {:msg (str "An unexpected failure while deleting: " e)}))) - -(defn delete! - "Deletes a page and then either calls the ok-handler or the error-handler if unable to delete" - [page-name ok-handler & {:keys [delete-file? redirect-to-home? persist-op? error-handler] - :or {delete-file? true - redirect-to-home? true - persist-op? true - error-handler (fn [{:keys [msg]}] (log/error :msg msg))}}] - (when redirect-to-home? (route-handler/redirect-to-home!)) - (when page-name - (when-let [repo (state/get-current-repo)] - (let [page-name (util/page-name-sanity-lc page-name) - blocks (db/get-page-blocks-no-cache page-name) - truncate-blocks-tx-data (mapv - (fn [block] - [:db.fn/retractEntity [:block/uuid (:block/uuid block)]]) - blocks) - page (db/entity [:block/name page-name])] - (if-let [msg (and (config/db-based-graph? repo) - (page-unable-to-delete repo page))] - (error-handler msg) - (let [_ (delete-file! repo page-name delete-file?) - ;; if other page alias this pagename, - ;; then just remove some attrs of this entity instead of retractEntity - delete-page-tx (cond - (not (:block/_namespace page)) - (if (model/get-alias-source-page (state/get-current-repo) page-name) - (when-let [id (:db/id (db/entity [:block/name page-name]))] - (mapv (fn [attribute] - [:db/retract id attribute]) - db-schema/retract-page-attributes)) - (concat (db-refs->page repo page) - [[:db.fn/retractEntity [:block/name page-name]]])) - - :else - nil) - tx-data (concat truncate-blocks-tx-data delete-page-tx)] - (db/transact! repo tx-data {:outliner-op :delete-page :persist-op? persist-op?}) - - (unfavorite-page! page-name) - - (when (fn? ok-handler) (ok-handler)) - (ui-handler/re-render-root!))))))) - -(defn- rename-update-block-refs! - [refs from-id to-id] - (->> refs - (remove #{{:db/id from-id}}) - (cons {:db/id to-id}) - (distinct) - (vec))) - -(defn- rename-update-refs! - "Unsanitized only" - [page old-original-name new-name] - ;; update all pages which have references to this page - (let [repo (state/get-current-repo) - to-page (db/entity [:block/name (util/page-name-sanity-lc new-name)]) - blocks (:block/_refs (db/entity (:db/id page))) - page-ids (->> (map (fn [b] - {:db/id (:db/id (:block/page b))}) blocks) - (set)) - tx (->> (map (fn [{:block/keys [uuid content properties format] :as block}] - (let [content (let [content' (replace-old-page! content old-original-name new-name format)] - (when-not (= content' content) - content')) - properties (let [properties' (walk-replace-old-page! properties old-original-name new-name format)] - (when-not (= properties' properties) - properties'))] - (when (or content properties) - (util/remove-nils-non-nested - {:block/uuid uuid - :block/content content - :block/properties properties - :block/properties-order (when (seq properties) - (map first properties)) - :block/refs (->> (rename-update-block-refs! (:block/refs block) (:db/id page) (:db/id to-page)) - (map :db/id) - (set))})))) blocks) - (remove nil?))] - (db/transact! repo tx) - (doseq [page-id page-ids] - (outliner-file/sync-to-file page-id)))) - -(defn- rename-update-namespace! - "update :block/namespace of the renamed block" - [page old-original-name new-name] - (let [old-namespace? (text/namespace-page? old-original-name) - new-namespace? (text/namespace-page? new-name) - repo (state/get-current-repo)] - (cond - new-namespace? - ;; update namespace - (let [namespace (first (gp-util/split-last "/" new-name))] - (when namespace - (create! namespace {:redirect? false}) ;; create parent page if not exist, creation of namespace ref is handled in `create!` - (let [namespace-block (db/pull [:block/name (gp-util/page-name-sanity-lc namespace)]) - page-txs [{:db/id (:db/id page) - :block/namespace (:db/id namespace-block)}]] - (db/transact! repo page-txs)))) - - old-namespace? - ;; retract namespace - (db/transact! [[:db/retract (:db/id page) :block/namespace]]) - - :else - nil))) - -(defn- rename-page-aux - "Only accepts unsanitized page names" - [old-name new-name redirect?] - (let [old-page-name (util/page-name-sanity-lc old-name) - new-file-name-body (fs-util/file-name-sanity new-name) ;; w/o file extension - new-page-name (util/page-name-sanity-lc new-name) - repo (state/get-current-repo) - page (db/pull [:block/name old-page-name])] - (when (and repo page) - (let [old-original-name (:block/original-name page) - file (:block/file page) - journal? (:block/journal? page) - properties-block (:data (outliner-tree/-get-down (outliner-core/block page))) - properties-content (:block/content properties-block) - properties-block-tx (when (and properties-block - properties-content - (string/includes? (util/page-name-sanity-lc properties-content) - old-page-name)) - (let [front-matter? (and (file-property/front-matter?-when-file-based properties-content) - (= :markdown (:block/format properties-block)))] - {:db/id (:db/id properties-block) - :block/content (file-property/insert-property - (:block/format properties-block) - properties-content - :title - new-name - front-matter?)})) - page-txs [{:db/id (:db/id page) - :block/uuid (:block/uuid page) - :block/name new-page-name - :block/original-name new-name}] - page-txs (if properties-block-tx (conj page-txs properties-block-tx) page-txs)] - - (db/transact! repo page-txs) - - (when (and (not (config/db-based-graph? repo)) - (fs-util/create-title-property? new-page-name)) - (page-property/add-property! new-page-name :title new-name)) - - (when (and file (not journal?)) - (rename-file! file new-file-name-body (fn [] nil))) - - (let [home (get (state/get-config) :default-home {})] - (when (= old-page-name (util/page-name-sanity-lc (get home :page ""))) - (config-handler/set-config! :default-home (assoc home :page new-name)))) - - (rename-update-refs! page old-original-name new-name) - - (rename-update-namespace! page old-original-name new-name) - - (outliner-file/sync-to-file page)) - - ;; Redirect to the newly renamed page - (when redirect? - (route-handler/redirect! {:to (if (model/whiteboard-page? page) :whiteboard :page) - :push false - :path-params {:name new-page-name}})) - - (when (favorited? old-page-name) - (p/do! - (unfavorite-page! old-page-name) - (favorite-page! new-page-name))) - - (recent-handler/update-or-add-renamed-page repo old-page-name new-page-name) - - (ui-handler/re-render-root!)))) - -(defn- rename-nested-pages - "Unsanitized names only" - [old-ns-name new-ns-name] - (let [repo (state/get-current-repo) - nested-page-str (page-ref/->page-ref (util/page-name-sanity-lc old-ns-name)) - ns-prefix-format-str (str page-ref/left-brackets "%s/") - ns-prefix (util/format ns-prefix-format-str (util/page-name-sanity-lc old-ns-name)) - nested-pages (db/get-pages-by-name-partition repo nested-page-str) - nested-pages-ns (db/get-pages-by-name-partition repo ns-prefix)] - (when nested-pages - ;; rename page "[[obsidian]] is a tool" to "[[logseq]] is a tool" - (doseq [{:block/keys [name original-name]} nested-pages] - (let [old-page-title (or original-name name) - new-page-title (string/replace - old-page-title - (page-ref/->page-ref old-ns-name) - (page-ref/->page-ref new-ns-name))] - (when (and old-page-title new-page-title) - (p/do! - (rename-page-aux old-page-title new-page-title false) - (println "Renamed " old-page-title " to " new-page-title)))))) - (when nested-pages-ns - ;; rename page "[[obsidian/page1]] is a tool" to "[[logseq/page1]] is a tool" - (doseq [{:block/keys [name original-name]} nested-pages-ns] - (let [old-page-title (or original-name name) - new-page-title (string/replace - old-page-title - (util/format ns-prefix-format-str old-ns-name) - (util/format ns-prefix-format-str new-ns-name))] - (when (and old-page-title new-page-title) - (p/do! - (rename-page-aux old-page-title new-page-title false) - (println "Renamed " old-page-title " to " new-page-title)))))))) - -(defn- rename-namespace-pages! - "Original names (unsanitized only)" - [repo old-name new-name] - (let [pages (db/get-namespace-pages repo old-name) - page (db/pull [:block/name (util/page-name-sanity-lc old-name)]) - pages (cons page pages)] - (doseq [{:block/keys [name original-name]} pages] - (let [old-page-title (or original-name name) - ;; only replace one time, for the case that the namespace is a sub-string of the sub-namespace page name - ;; Example: has pages [[work]] [[work/worklog]], - ;; we want to rename [[work/worklog]] to [[work1/worklog]] when rename [[work]] to [[work1]], - ;; but don't rename [[work/worklog]] to [[work1/work1log]] - new-page-title (string/replace-first old-page-title old-name new-name) - redirect? (= name (:block/name page))] - (when (and old-page-title new-page-title) - (p/let [_ (rename-page-aux old-page-title new-page-title redirect?)] - (println "Renamed " old-page-title " to " new-page-title))))))) - -(defn file-based-merge-pages! - "Only accepts sanitized page names" - [from-page-name to-page-name] - (when (and (db/page-exists? from-page-name) - (db/page-exists? to-page-name) - (not= from-page-name to-page-name)) - (let [to-page (db/entity [:block/name to-page-name]) - to-id (:db/id to-page) - from-page (db/entity [:block/name from-page-name]) - from-id (:db/id from-page) - from-first-child (some->> (db/pull from-id) - (outliner-core/block) - (outliner-tree/-get-down) - (outliner-core/get-data)) - to-last-direct-child-id (model/get-block-last-direct-child (db/get-db) to-id false) - repo (state/get-current-repo) - conn (conn/get-db repo false) - datoms (d/datoms @conn :avet :block/page from-id) - block-eids (mapv :e datoms) - blocks (db-utils/pull-many repo '[:db/id :block/page :block/refs :block/path-refs :block/left :block/parent] block-eids) - tx-data (map (fn [block] - (let [id (:db/id block)] - (cond-> - {:db/id id - :block/page {:db/id to-id} - :block/refs (rename-update-block-refs! (:block/refs block) from-id to-id)} - - (and from-first-child (= id (:db/id from-first-child))) - (assoc :block/left {:db/id (or to-last-direct-child-id to-id)}) - - (= (:block/parent block) {:db/id from-id}) - (assoc :block/parent {:db/id to-id})))) blocks)] - (db/transact! repo tx-data) - (outliner-file/sync-to-file {:db/id to-id}) - - (rename-update-refs! from-page - (util/get-page-original-name from-page) - (util/get-page-original-name to-page)) - - (rename-update-namespace! from-page - (util/get-page-original-name from-page) - (util/get-page-original-name to-page))) - - - (delete! from-page-name nil) - - (route-handler/redirect! {:to :page - :push false - :path-params {:name to-page-name}}))) - -(defn db-based-merge-pages! - [from-page-name to-page-name persist-op?] - (when (and (db/page-exists? from-page-name) - (db/page-exists? to-page-name) - (not= from-page-name to-page-name)) - (let [to-page (db/entity [:block/name to-page-name]) - to-id (:db/id to-page) - from-page (db/entity [:block/name from-page-name]) - from-id (:db/id from-page) - from-first-child (some->> (db/pull from-id) - (outliner-core/block) - (outliner-tree/-get-down) - (outliner-core/get-data)) - to-last-direct-child-id (model/get-block-last-direct-child (db/get-db) to-id false) - repo (state/get-current-repo) - conn (conn/get-db repo false) - datoms (d/datoms @conn :avet :block/page from-id) - block-eids (mapv :e datoms) - blocks (db-utils/pull-many repo '[:db/id :block/page :block/refs :block/path-refs :block/left :block/parent] block-eids) - blocks-tx-data (map (fn [block] - (let [id (:db/id block)] - (cond-> - {:db/id id - :block/page {:db/id to-id}} - - (and from-first-child (= id (:db/id from-first-child))) - (assoc :block/left {:db/id (or to-last-direct-child-id to-id)}) - - (= (:block/parent block) {:db/id from-id}) - (assoc :block/parent {:db/id to-id})))) blocks) - replace-ref-tx-data (db-replace-ref repo from-page to-page) - tx-data (concat blocks-tx-data replace-ref-tx-data)] - (db/transact! repo tx-data {:persist-op? persist-op?}) - (rename-update-namespace! from-page - (util/get-page-original-name from-page) - (util/get-page-original-name to-page))) - - - (delete! from-page-name nil :redirect-to-home? false :persist-op? persist-op?) - - (route-handler/redirect! {:to :page - :push false - :path-params {:name to-page-name}}))) - -(defn db-based-rename! - ([old-name new-name] - (db-based-rename! old-name new-name true true)) - ([old-name new-name redirect? persist-op?] - (let [repo (state/get-current-repo) - old-name (string/trim old-name) - new-name (string/trim new-name) - old-page-name (util/page-name-sanity-lc old-name) - page-e (db/entity [:block/name old-page-name]) - new-page-name (util/page-name-sanity-lc new-name) - name-changed? (not= old-name new-name)] - (if (and old-name - new-name - (not (string/blank? new-name)) - name-changed?) - (cond - (= old-page-name new-page-name) ; case changed - (db/transact! repo - [{:db/id (:db/id page-e) - :block/original-name new-name}] - {:persist-op? persist-op?}) - - (and (not= old-page-name new-page-name) - (db/entity [:block/name new-page-name])) ; merge page - (db-based-merge-pages! old-page-name new-page-name persist-op?) - - :else ; rename - (create! new-name - {:rename? true - :uuid (:block/uuid page-e) - :redirect? redirect? - :create-first-block? false - :persist-op? persist-op?})) - - (when (string/blank? new-name) - (notification/show! "Please use a valid name, empty name is not allowed!" :error))) - (ui-handler/re-render-root!)))) - -(defn file-based-rename! - "Accepts unsanitized page names" - ([old-name new-name] (file-based-rename! old-name new-name true)) - ([old-name new-name redirect?] - (let [repo (state/get-current-repo) - old-name (string/trim old-name) - new-name (string/trim new-name) - old-page-name (util/page-name-sanity-lc old-name) - new-page-name (util/page-name-sanity-lc new-name) - name-changed? (not= old-name new-name)] - (if (and old-name - new-name - (not (string/blank? new-name)) - name-changed?) - (do - (cond - (= old-page-name new-page-name) - (rename-page-aux old-name new-name redirect?) - - (db/pull [:block/name new-page-name]) - (file-based-merge-pages! old-page-name new-page-name) - - :else - (rename-namespace-pages! repo old-name new-name)) - (rename-nested-pages old-name new-name)) - (when (string/blank? new-name) - (notification/show! "Please use a valid name, empty name is not allowed!" :error))) - (ui-handler/re-render-root!)))) - (defn rename! ([old-name new-name] (rename! old-name new-name true)) ([old-name new-name redirect?] (rename! old-name new-name redirect? true)) ([old-name new-name redirect? persist-op?] (if (config/db-based-graph? (state/get-current-repo)) - (db-based-rename! old-name new-name redirect? persist-op?) - (file-based-rename! old-name new-name redirect?)))) + (db-page-handler/rename! old-name new-name redirect? persist-op?) + (file-page-handler/rename! old-name new-name redirect?)))) (defn reorder-favorites! [{:keys [to up?]}] diff --git a/src/main/frontend/handler/recent.cljs b/src/main/frontend/handler/recent.cljs index 83d3b0e5c6..4bab892f0e 100644 --- a/src/main/frontend/handler/recent.cljs +++ b/src/main/frontend/handler/recent.cljs @@ -1,7 +1,7 @@ (ns frontend.handler.recent "Fns related to recent pages feature" (:require [frontend.handler.db-based.recent :as db-based] - [frontend.handler.file-based.recent :as file-based] + [frontend.handler.file-based.recent :as file-recent-handler] [frontend.config :as config] [frontend.state :as state])) @@ -9,11 +9,11 @@ [repo page click-from-recent?] (if (config/db-based-graph? repo) (db-based/add-page-to-recent! repo page click-from-recent?) - (file-based/add-page-to-recent! repo page click-from-recent?))) + (file-recent-handler/add-page-to-recent! repo page click-from-recent?))) (defn get-recent-pages [] (let [repo (state/get-current-repo)] (if (config/db-based-graph? repo) (db-based/get-recent-pages) - (file-based/get-recent-pages)))) + (file-recent-handler/get-recent-pages)))) diff --git a/src/test/frontend/db/name_sanity_test.cljs b/src/test/frontend/db/name_sanity_test.cljs index 99863950ab..e55e272308 100644 --- a/src/test/frontend/db/name_sanity_test.cljs +++ b/src/test/frontend/db/name_sanity_test.cljs @@ -2,7 +2,7 @@ (:require [cljs.test :refer [deftest testing is are]] [clojure.string :as string] [logseq.graph-parser.util :as gp-util] - [frontend.handler.page :as page-handler] + [frontend.handler.file-based.page :as file-page-handler] [frontend.handler.conversion :as conversion-handler] [frontend.util.fs :as fs-util])) @@ -46,8 +46,8 @@ (mapv test-page-name fs-util/windows-reserved-filebodies)) (deftest new-path-computation-tests - (is (= (#'page-handler/compute-new-file-path "/data/app/dsal dsalfjk aldsaf.jkl" "ddd") "/data/app/ddd.jkl")) - (is (= (#'page-handler/compute-new-file-path "c://data/a sdfpp/dsal dsalf% * _ dsaf.mnk" "c d / f") "c://data/a sdfpp/c d / f.mnk"))) + (is (= (#'file-page-handler/compute-new-file-path "/data/app/dsal dsalfjk aldsaf.jkl" "ddd") "/data/app/ddd.jkl")) + (is (= (#'file-page-handler/compute-new-file-path "c://data/a sdfpp/dsal dsalf% * _ dsaf.mnk" "c d / f") "c://data/a sdfpp/c d / f.mnk"))) (deftest break-change-conversion-tests (let [conv-legacy #(:target (#'conversion-handler/calc-previous-name :legacy :triple-lowbar %))] diff --git a/src/test/frontend/handler/page_test.cljs b/src/test/frontend/handler/file_based/page_test.cljs similarity index 95% rename from src/test/frontend/handler/page_test.cljs rename to src/test/frontend/handler/file_based/page_test.cljs index 5467986e64..374dbd012e 100644 --- a/src/test/frontend/handler/page_test.cljs +++ b/src/test/frontend/handler/file_based/page_test.cljs @@ -1,10 +1,10 @@ -(ns frontend.handler.page-test +(ns frontend.handler.file-based.page-test ;; namespace local config for private function tests {:clj-kondo/config {:linters {:private-call {:level :off}}}} (:require [cljs.test :refer [deftest are]] [clojure.string :as string] [frontend.util :as util] - [frontend.handler.page :as page-handler])) + [frontend.handler.file-based.page :as file-page-handler])) (defn- replace-page-ref! [content old-name new-name] @@ -36,7 +36,7 @@ (when (and (string? content) (string? old-name) (string? new-name)) (-> content (replace-page-ref! old-name new-name) - (page-handler/replace-tag-ref! old-name new-name)))) + (file-page-handler/replace-tag-ref! old-name new-name)))) (deftest test-replace-page-ref! (are [x y] (= (let [[content old-name new-name] x] @@ -66,7 +66,7 @@ (deftest test-replace-tag-ref! (are [x y] (= (let [[content old-name new-name] x] - (page-handler/replace-tag-ref! content old-name new-name)) + (file-page-handler/replace-tag-ref! content old-name new-name)) y) ["#foo" "foo" "bar"] "#bar" ["#foo" "foo" "new bar"] "#[[new bar]]" diff --git a/src/test/frontend/handler/repo_conversion_test.cljs b/src/test/frontend/handler/repo_conversion_test.cljs index 03ace14d9f..7e0afc6e13 100644 --- a/src/test/frontend/handler/repo_conversion_test.cljs +++ b/src/test/frontend/handler/repo_conversion_test.cljs @@ -7,7 +7,7 @@ [logseq.graph-parser.test.docs-graph-helper :as docs-graph-helper] [logseq.graph-parser.config :as gp-config] [frontend.test.helper :as test-helper] - [frontend.handler.page :as page-handler] + [frontend.handler.file-based.page :as file-page-handler] [frontend.handler.conversion :as conversion-handler] [frontend.handler.repo :as repo-handler] [frontend.db.conn :as conn] @@ -123,7 +123,7 @@ (if rename-target #_:clj-kondo/ignore (do #_(prn "conversion triple-lowbar: " original-body " -> " rename-target) - (#'page-handler/compute-new-file-path path rename-target)) + (#'file-page-handler/compute-new-file-path path rename-target)) path))) (defn- convert-graph-files-path