| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- (ns frontend.components.repo
- (:require [clojure.string :as string]
- [frontend.common.async-util :as async-util]
- [frontend.config :as config]
- [frontend.context.i18n :refer [t]]
- [frontend.db :as db]
- [frontend.handler.db-based.rtc :as rtc-handler]
- [frontend.handler.db-based.rtc-flows :as rtc-flows]
- [frontend.handler.file-based.nfs :as nfs-handler]
- [frontend.handler.file-sync :as file-sync]
- [frontend.handler.graph :as graph]
- [frontend.handler.notification :as notification]
- [frontend.handler.repo :as repo-handler]
- [frontend.handler.route :as route-handler]
- [frontend.handler.user :as user-handler]
- [frontend.mobile.util :as mobile-util]
- [frontend.state :as state]
- [frontend.ui :as ui]
- [frontend.util :as util]
- [frontend.util.fs :as fs-util]
- [frontend.util.text :as text-util]
- [goog.object :as gobj]
- [lambdaisland.glogi :as log]
- [logseq.shui.ui :as shui]
- [medley.core :as medley]
- [promesa.core :as p]
- [rum.core :as rum]))
- (rum/defc normalized-graph-label
- [{:keys [url remote? GraphName GraphUUID] :as graph} on-click]
- (when graph
- [:span.flex.items-center
- (if (or (config/local-file-based-graph? url)
- (config/db-based-graph? url))
- (let [local-dir (config/get-local-dir url)
- graph-name (text-util/get-graph-name-from-path url)]
- [:a.flex.items-center {:title local-dir
- :on-click #(on-click graph)}
- [:span graph-name (when GraphName [:strong.px-1 "(" GraphName ")"])]
- (when remote? [:strong.pr-1.flex.items-center (ui/icon "cloud")])])
- [:a.flex.items-center {:title GraphUUID
- :on-click #(on-click graph)}
- (db/get-repo-path (or url GraphName))
- (when remote? [:strong.pl-1.flex.items-center (ui/icon "cloud")])])]))
- (defn sort-repos-with-metadata-local
- [repos]
- (if-let [m (and (seq repos) (graph/get-metadata-local))]
- (->> repos
- (map (fn [r] (merge r (get m (:url r)))))
- (sort (fn [r1 r2]
- (compare (or (:last-seen-at r2) (:created-at r2))
- (or (:last-seen-at r1) (:created-at r1))))))
- repos))
- (defn- safe-locale-date
- [dst]
- (when (number? dst)
- (try
- (.toLocaleString (js/Date. dst))
- (catch js/Error _e nil))))
- (rum/defc repos-inner
- "Graph list in `All graphs` page"
- [repos]
- (for [{:keys [root url remote? GraphUUID GraphSchemaVersion GraphName created-at last-seen-at] :as repo}
- (sort-repos-with-metadata-local repos)
- :let [only-cloud? (and remote? (nil? root))
- db-based? (config/db-based-graph? url)]]
- [:div.flex.justify-between.mb-4.items-center.group {:key (or url GraphUUID)}
- [:div
- [:span.flex.items-center.gap-1
- (normalized-graph-label repo
- (fn []
- (when-not (state/sub :rtc/downloading-graph-uuid)
- (cond
- root ; exists locally
- (state/pub-event! [:graph/switch url])
- (and db-based? remote?)
- (state/pub-event! [:rtc/download-remote-graph GraphName GraphUUID GraphSchemaVersion])
- :else
- (state/pub-event! [:graph/pull-down-remote-graph repo])))))]
- (when-let [time (some-> (or last-seen-at created-at) (safe-locale-date))]
- [:small.text-gray-400.opacity-50 (str "Last opened at: " time)])]
- [:div.controls
- [:div.flex.flex-row.items-center
- (when (util/electron?)
- [:a.text-xs.items-center.text-gray-08.hover:underline.hidden.group-hover:flex
- {:on-click #(util/open-url (str "file://" root))}
- (shui/tabler-icon "folder-pin") [:span.pl-1 root]])
- (let [db-graph? (config/db-based-graph? url)
- manager? (and db-graph? (user-handler/manager? url))
- title (cond
- only-cloud?
- "Deletes this remote graph. Note this can't be recovered."
- db-based?
- "Unsafe delete this DB-based graph. Note this can't be recovered."
- :else
- "Removes Logseq's access to the local file path of your graph. It won't remove your local files.")]
- (when-not (and only-cloud? (not manager?))
- [:a.text-gray-400.ml-4.font-medium.text-sm.whitespace-nowrap
- {:title title
- :on-click (fn []
- (let [has-prompt? true
- prompt-str (cond only-cloud?
- (str "Are you sure to permanently delete the graph \"" GraphName "\" from our server?")
- db-based?
- (str "Are you sure to permanently delete the graph \"" url "\" from Logseq?")
- :else
- (str "Are you sure to unlink the graph \"" url "\" from local folder?"))
- unlink-or-remote-fn! (fn []
- (repo-handler/remove-repo! repo)
- (state/pub-event! [:graph/unlinked repo (state/get-current-repo)]))
- action-confirm-fn! (if only-cloud?
- (fn []
- (when (or manager? (not db-graph?))
- (let [<delete-graph (if db-graph?
- rtc-handler/<rtc-delete-graph!
- (fn [graph-uuid _graph-schema-version]
- (async-util/c->p (file-sync/<delete-graph graph-uuid))))]
- (state/set-state! [:file-sync/remote-graphs :loading] true)
- (p/do! (<delete-graph GraphUUID GraphSchemaVersion)
- (state/delete-repo! repo)
- (state/delete-remote-graph! repo)
- (state/set-state! [:file-sync/remote-graphs :loading] false)))))
- unlink-or-remote-fn!)
- confirm-fn!
- (fn []
- (-> (shui/dialog-confirm!
- [:p.font-medium.-my-4 prompt-str
- [:span.mt-1.flex.font-normal.opacity-70
- (if (or db-based? only-cloud?)
- [:small.text-red-rx-11 "⚠️ Notice that we can't recover this graph after being deleted. Make sure you have backups before deleting it."]
- [:small.opacity-70 "⚠️ It won't remove your local files!"])]])
- (p/then #(action-confirm-fn!))))]
- (if has-prompt?
- (confirm-fn!)
- (unlink-or-remote-fn!))))}
- (if only-cloud? "Remove (server)" "Unlink (local)")]))]]]))
- (rum/defc repos-cp < rum/reactive
- []
- (let [login? (boolean (state/sub :auth/id-token))
- repos (state/sub [:me :repos])
- repos (util/distinct-by :url repos)
- remotes (concat
- (state/sub :rtc/graphs)
- (state/sub [:file-sync/remote-graphs :graphs]))
- remotes-loading? (state/sub [:file-sync/remote-graphs :loading])
- repos (if (and login? (seq remotes))
- (repo-handler/combine-local-&-remote-graphs repos remotes) repos)
- repos (remove #(= (:url %) config/demo-repo) repos)
- {remote-graphs true local-graphs false} (group-by (comp boolean :remote?) repos)]
- [:div#graphs
- [:h1.title (t :graph/all-graphs)]
- [:div.pl-1.content.mt-3
- [:div
- [:h2.text-lg.font-medium.my-4 (t :graph/local-graphs)]
- (when (seq local-graphs)
- (repos-inner local-graphs))
- [:div.flex.flex-row.my-4
- (if util/web-platform?
- [:div.mr-8
- (ui/button
- "Create a new graph"
- :on-click #(state/pub-event! [:graph/new-db-graph]))]
- (when (or (nfs-handler/supported?)
- (mobile-util/native-platform?))
- [:div.mr-8
- (ui/button
- (t :open-a-directory)
- :on-click #(state/pub-event! [:graph/setup-a-repo]))]))]]
- (when (and (or (file-sync/enable-sync?)
- (state/enable-rtc?))
- login?)
- [:div
- [:hr]
- [:div.flex.align-items.justify-between
- [:h2.text-lg.font-medium.my-4 (t :graph/remote-graphs)]
- [:div
- (ui/button
- [:span.flex.items-center "Refresh"
- (when remotes-loading? [:small.pl-2 (ui/loading nil)])]
- :background "gray"
- :disabled remotes-loading?
- :on-click (fn []
- (file-sync/load-session-graphs)
- (p/do!
- (rtc-handler/<get-remote-graphs)
- (repo-handler/refresh-repos!))))]]
- (repos-inner remote-graphs)])]]))
- (defn- repos-dropdown-links [repos current-repo downloading-graph-id & {:as opts}]
- (let [switch-repos (if-not (nil? current-repo)
- (remove (fn [repo] (= current-repo (:url repo))) repos) repos) ; exclude current repo
- repo-links (mapv
- (fn [{:keys [url remote? rtc-graph? GraphName GraphSchemaVersion GraphUUID] :as graph}]
- (let [local? (config/local-file-based-graph? url)
- db-only? (config/db-based-graph? url)
- repo-url (cond
- local? (db/get-repo-name url)
- db-only? url
- :else GraphName)
- short-repo-name (if (or local? db-only?)
- (text-util/get-graph-name-from-path repo-url)
- GraphName)
- downloading? (and downloading-graph-id (= GraphUUID downloading-graph-id))]
- (when short-repo-name
- {:title [:span.flex.items-center.title-wrap short-repo-name
- (when remote? [:span.pl-1.flex.items-center
- {:title (str "<" GraphName "> #" GraphUUID)}
- (ui/icon "cloud" {:size 18})
- (when downloading?
- [:span.opacity.text-sm.pl-1 "downloading"])])]
- :hover-detail repo-url ;; show full path on hover
- :options {:on-click
- (fn [e]
- (when-not downloading?
- (when-let [on-click (:on-click opts)]
- (on-click e))
- (if (and (gobj/get e "shiftKey")
- (not (and rtc-graph? remote?)))
- (state/pub-event! [:graph/open-new-window url])
- (cond
- ;; exists locally?
- (or (:root graph)
- (and db-only? (not rtc-graph?)))
- (state/pub-event! [:graph/switch url])
- (and rtc-graph? remote?)
- (state/pub-event!
- [:rtc/download-remote-graph GraphName GraphUUID GraphSchemaVersion])
- :else
- (state/pub-event! [:graph/pull-down-remote-graph graph])))))}})))
- switch-repos)]
- (->> repo-links (remove nil?))))
- (defn- repos-footer [multiple-windows? db-based?]
- [:div.cp__repos-quick-actions
- {:on-click #(shui/popup-hide!)}
- (when (and (not db-based?)
- (not (config/demo-graph?)))
- [:<>
- (shui/button {:size :sm :variant :ghost
- :title (t :sync-from-local-files-detail)
- :on-click (fn []
- (state/pub-event! [:graph/ask-for-re-fresh]))}
- (shui/tabler-icon "file-report") [:span (t :sync-from-local-files)])
- (shui/button {:size :sm :variant :ghost
- :title (t :re-index-detail)
- :on-click (fn []
- (state/pub-event! [:graph/ask-for-re-index multiple-windows? nil]))}
- (shui/tabler-icon "folder-bolt") [:span (t :re-index)])])
- (when (util/electron?)
- (shui/button {:size :sm :variant :ghost
- :on-click (fn []
- (if (or (nfs-handler/supported?) (mobile-util/native-platform?))
- (state/pub-event! [:graph/setup-a-repo])
- (route-handler/redirect-to-all-graphs)))}
- (shui/tabler-icon "folder-plus")
- [:span (t :new-graph)]))
- (when-not config/publishing?
- (shui/button
- {:size :sm :variant :ghost
- :on-click #(state/pub-event! [:graph/new-db-graph])}
- (shui/tabler-icon "database-plus")
- [:span (if util/electron? "Create db graph" "Create new graph")]))
- (when-not config/publishing?
- (shui/button
- {:size :sm :variant :ghost
- :on-click (fn [] (route-handler/redirect! {:to :import}))}
- (shui/tabler-icon "database-import")
- [:span (t :import-notes)]))
- (when-not config/publishing?
- (shui/button {:size :sm :variant :ghost
- :on-click #(route-handler/redirect-to-all-graphs)}
- (shui/tabler-icon "layout-2") [:span (t :all-graphs)]))])
- (rum/defcs repos-dropdown-content < rum/reactive
- [_state & {:keys [contentid] :as opts}]
- (let [multiple-windows? false
- current-repo (state/sub :git/current-repo)
- login? (boolean (state/sub :auth/id-token))
- repos (state/sub [:me :repos])
- remotes (state/sub [:file-sync/remote-graphs :graphs])
- rtc-graphs (state/sub :rtc/graphs)
- downloading-graph-id (state/sub :rtc/downloading-graph-uuid)
- remotes-loading? (state/sub [:file-sync/remote-graphs :loading])
- db-based? (config/db-based-graph? current-repo)
- repos (sort-repos-with-metadata-local repos)
- repos (distinct
- (if (and (or (seq remotes) (seq rtc-graphs)) login?)
- (repo-handler/combine-local-&-remote-graphs repos (concat remotes rtc-graphs)) repos))
- items-fn #(repos-dropdown-links repos current-repo downloading-graph-id opts)
- header-fn #(when (> (count repos) 1) ; show switch to if there are multiple repos
- [:div.font-medium.text-sm.opacity-50.px-1.py-1.flex.flex-row.justify-between.items-center
- [:h4.pb-1 (t :left-side-bar/switch)]
- (when (and (file-sync/enable-sync?) login?)
- (if remotes-loading?
- (ui/loading "")
- (shui/button
- {:variant :ghost
- :size :sm
- :title "Refresh remote graphs"
- :class "!h-6 !px-1 relative right-[-4px]"
- :on-click (fn []
- (file-sync/load-session-graphs)
- (rtc-handler/<get-remote-graphs))}
- (ui/icon "refresh" {:size 15}))))])
- _remote? (and current-repo (:remote? (first (filter #(= current-repo (:url %)) repos))))
- _repo-name (when current-repo (db/get-repo-name current-repo))]
- [:div
- {:class (when (<= (count repos) 1) "no-repos")}
- (header-fn)
- [:div.cp__repos-list-wrap
- (for [{:keys [hr item hover-detail title options icon]} (items-fn)]
- (let [on-click' (:on-click options)
- href' (:href options)]
- (if hr
- (shui/dropdown-menu-separator)
- (shui/dropdown-menu-item
- (assoc options
- :title hover-detail
- :on-click (fn [^js e]
- (when on-click'
- (when-not (false? (on-click' e))
- (shui/popup-hide! contentid)))))
- (or item
- (if href'
- [:a.flex.items-center.w-full
- {:href href' :on-click #(shui/popup-hide! contentid)
- :style {:color "inherit"}} title]
- [:span.flex.items-center.gap-1.w-full
- icon [:div title]]))))))]
- (repos-footer multiple-windows? db-based?)]))
- (rum/defcs graphs-selector < rum/reactive
- [_state]
- (let [current-repo (state/get-current-repo)
- user-repos (state/get-repos)
- current-repo' (some->> user-repos (medley/find-first #(= current-repo (:url %))))
- repo-name (when current-repo (db/get-repo-name current-repo))
- db-based? (config/db-based-graph? current-repo)
- remote? (:remote? current-repo')
- short-repo-name (if current-repo
- (db/get-short-repo-name repo-name)
- "Select a Graph")]
- [:div.cp__graphs-selector.flex.items-center.justify-between
- [:a.item.flex.items-center.gap-1.select-none
- {:title current-repo
- :on-click (fn [^js e]
- (shui/popup-show! (.closest (.-target e) "a")
- (fn [{:keys [id]}] (repos-dropdown-content {:contentid id}))
- {:as-dropdown? true
- :content-props {:class "repos-list"}
- :align :start}))}
- [:span.thumb (shui/tabler-icon (if remote? "cloud" (if db-based? "database" "folder")) {:size 16})]
- [:strong short-repo-name]
- (shui/tabler-icon "selector" {:size 18})]]))
- (defn invalid-graph-name-warning
- []
- (notification/show!
- [:div
- [:p "Graph name can't contain following reserved characters:"]
- [:ul
- [:li "< (less than)"]
- [:li "> (greater than)"]
- [:li ": (colon)"]
- [:li "\" (double quote)"]
- [:li "/ (forward slash)"]
- [:li "\\ (backslash)"]
- [:li "| (vertical bar or pipe)"]
- [:li "? (question mark)"]
- [:li "* (asterisk)"]
- [:li "# (hash)"]
- ;; `+` is used to encode path that includes `:` or `/`
- [:li "+ (plus)"]]]
- :warning false))
- (defn invalid-graph-name?
- "Returns boolean indicating if DB graph name is invalid. Must be kept in sync with invalid-graph-name-warning"
- [graph-name]
- (or (fs-util/include-reserved-chars? graph-name)
- (string/includes? graph-name "+")
- (string/includes? graph-name "/")))
- (rum/defcs new-db-graph < rum/reactive
- (rum/local "" ::graph-name)
- (rum/local false ::cloud?)
- (rum/local false ::creating-db?)
- (rum/local (rum/create-ref) ::input-ref)
- {:did-mount (fn [s]
- (when-let [^js input (some-> @(::input-ref s)
- (rum/deref))]
- (js/setTimeout #(.focus input) 32))
- s)}
- [state]
- (let [*creating-db? (::creating-db? state)
- *graph-name (::graph-name state)
- *cloud? (::cloud? state)
- input-ref @(::input-ref state)
- new-db-f (fn []
- (when-not (or (string/blank? @*graph-name)
- @*creating-db?)
- (if (invalid-graph-name? @*graph-name)
- (invalid-graph-name-warning)
- (do
- (reset! *creating-db? true)
- (p/let [repo (repo-handler/new-db! @*graph-name)]
- (when @*cloud?
- (->
- (p/do
- (state/set-state! :rtc/uploading? true)
- (rtc-handler/<rtc-create-graph! repo)
- (state/set-state! :rtc/uploading? false)
- (rtc-flows/trigger-rtc-start repo))
- (p/catch (fn [error]
- (reset! *creating-db? false)
- (state/set-state! :rtc/uploading? false)
- (log/error :create-db-failed error)))))
- (reset! *creating-db? false)
- (shui/dialog-close!))))))
- submit! (fn [^js e click?]
- (when-let [value (and (or click? (= (gobj/get e "key") "Enter"))
- (util/trim-safe (.-value (rum/deref input-ref))))]
- (reset! *graph-name value)
- (new-db-f)))]
- [:div.new-graph.flex.flex-col.gap-4.p-1.pt-2
- (shui/input
- {:default-value @*graph-name
- :disabled @*creating-db?
- :ref input-ref
- :placeholder "your graph name"
- :on-key-down submit!})
- (when (user-handler/team-member?)
- [:div.flex.flex-row.items-center.gap-1
- (shui/checkbox
- {:id "rtc-sync"
- :value @*cloud?
- :on-checked-change #(swap! *cloud? not)})
- [:label.opacity-70.text-sm
- {:for "rtc-sync"}
- "Use Logseq Sync?"]])
- (shui/button
- {:on-click #(submit! % true)
- :on-key-down submit!}
- (if @*creating-db?
- (ui/loading "Creating graph")
- "Submit"))]))
|