File history versions management

This commit is contained in:
alonso.torres 2024-10-15 12:11:32 +02:00
parent fa4f2aa5cc
commit ecb7f0a2f6
33 changed files with 1100 additions and 102 deletions

View file

@ -106,7 +106,7 @@
(defn commit
"Create a commit event instance"
[{:keys [commit-id redo-changes undo-changes origin save-undo? features
file-id file-revn undo-group tags stack-undo? source]}]
file-id file-revn file-vern undo-group tags stack-undo? source]}]
(dm/assert!
"expect valid vector of changes for redo-changes"
@ -126,6 +126,7 @@
:features features
:file-id file-id
:file-revn file-revn
:file-vern file-vern
:changes redo-changes
:redo-changes redo-changes
:undo-changes undo-changes
@ -160,6 +161,13 @@
(:revn file)
(dm/get-in state [:workspace-libraries file-id :revn]))))
(defn- resolve-file-vern
[state file-id]
(let [file (:workspace-file state)]
(if (= (:id file) file-id)
(:vern file)
(dm/get-in state [:workspace-libraries file-id :vern]))))
(defn commit-changes
"Schedules a list of changes to execute now, and add the corresponding undo changes to
the undo stack.
@ -194,6 +202,7 @@
(assoc :save-undo? save-undo?)
(assoc :file-id file-id)
(assoc :file-revn (resolve-file-revn state file-id))
(assoc :file-vern (resolve-file-vern state file-id))
(assoc :undo-changes uchg)
(assoc :redo-changes rchg)
(commit))))))))

View file

@ -108,11 +108,12 @@
ptk/WatchEvent
(watch [_ state _]
(log/dbg :hint "persist-commit" :commit-id (dm/str commit-id))
(when-let [{:keys [file-id file-revn changes features] :as commit} (dm/get-in state [:persistence :index commit-id])]
(when-let [{:keys [file-id file-revn file-vern changes features] :as commit} (dm/get-in state [:persistence :index commit-id])]
(let [sid (:session-id state)
revn (max file-revn (get @revn-data file-id 0))
params {:id file-id
:revn revn
:vern file-vern
:session-id sid
:origin (:origin commit)
:created-at (:created-at commit)

View file

@ -70,6 +70,7 @@
[app.main.data.workspace.undo :as dwu]
[app.main.data.workspace.viewport :as dwv]
[app.main.data.workspace.zoom :as dwz]
[app.main.errors]
[app.main.features :as features]
[app.main.features.pointer-map :as fpmap]
[app.main.repo :as rp]
@ -378,6 +379,19 @@
(let [name (dm/str "workspace-" file-id)]
(unchecked-set ug/global "name" name)))))
(defn reload-file
[]
(ptk/reify ::reload-file
ptk/WatchEvent
(watch [_ state _]
(let [file-id (:current-file-id state)
project-id (:current-project-id state)]
(rx/of (initialize-file project-id file-id))))))
;; We need to inject this so there are no cycles
(set! app.main.data.workspace.notifications/reload-file reload-file)
(set! app.main.errors/reload-file reload-file)
(defn finalize-file
[_project-id file-id]
(ptk/reify ::finalize-file

View file

@ -29,12 +29,16 @@
[clojure.set :as set]
[potok.v2.core :as ptk]))
;; From app.main.data.workspace we can use directly because it causes a circular dependency
(def reload-file nil)
;; FIXME: this ns should be renamed to something different
(declare process-message)
(declare handle-presence)
(declare handle-pointer-update)
(declare handle-file-change)
(declare handle-file-restore)
(declare handle-library-change)
(declare handle-pointer-send)
(declare handle-export-update)
@ -124,6 +128,7 @@
:disconnect (handle-presence msg)
:pointer-update (handle-pointer-update msg)
:file-change (handle-file-change msg)
:file-restore (handle-file-restore msg)
:library-change (handle-library-change msg)
:notification (dc/handle-notification msg)
:team-role-change (handle-change-team-role msg)
@ -229,13 +234,14 @@
[:file-id ::sm/uuid]
[:session-id ::sm/uuid]
[:revn :int]
[:vern :int]
[:changes ::cpc/changes]])
(def ^:private check-file-change-params!
(sm/check-fn schema:handle-file-change))
(defn handle-file-change
[{:keys [file-id changes revn] :as msg}]
[{:keys [file-id changes revn vern] :as msg}]
(dm/assert!
"expected valid parameters"
@ -250,13 +256,41 @@
;; The commit event is responsible to apply the data localy
;; and update the persistence internal state with the updated
;; file-revn
(rx/of (dch/commit {:file-id file-id
:file-revn revn
:file-vern vern
:save-undo? false
:source :remote
:redo-changes (vec changes)
:undo-changes []})))))
(def ^:private
schema:handle-file-restore
[:map {:title "handle-file-restore"}
[:type :keyword]
[:file-id ::sm/uuid]
[:vern :int]])
(def ^:private check-file-restore-params!
(sm/check-fn schema:handle-file-restore))
(defn handle-file-restore
[{:keys [file-id vern] :as msg}]
(dm/assert!
"expected valid parameters"
(check-file-restore-params! msg))
(ptk/reify ::handle-file-restore
ptk/WatchEvent
(watch [_ state _]
(let [curr-file-id (:current-file-id state)
curr-vern (dm/get-in state [:workspace-file :vern])
reload? (and (= file-id curr-file-id) (not= vern curr-vern))]
(when reload?
(rx/of (reload-file)))))))
(def ^:private schema:handle-library-change
[:map {:title "handle-library-change"}
[:type :keyword]

View file

@ -0,0 +1,131 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.data.workspace.versions
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.data.persistence :as dwp]
[app.main.data.workspace :as dw]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.util.time :as dt]
[beicon.v2.core :as rx]
[potok.v2.core :as ptk]))
(defonce default-state
{:status :loading
:data nil
:editing nil})
(declare fetch-versions)
(defn init-version-state
[file-id]
(ptk/reify ::init-version-state
ptk/UpdateEvent
(update [_ state]
(assoc state :workspace-versions default-state))
ptk/WatchEvent
(watch [_ _ _]
(rx/of (fetch-versions file-id)))))
(defn update-version-state
[version-state]
(ptk/reify ::update-version-state
ptk/UpdateEvent
(update [_ state]
(update state :workspace-versions merge version-state))))
(defn fetch-versions
[file-id]
(dm/assert! (uuid? file-id))
(ptk/reify ::fetch-versions
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :get-file-snapshots {:file-id file-id})
(rx/map #(update-version-state {:status :loaded :data %}))))))
(defn create-version
[file-id]
(dm/assert! (uuid? file-id))
(ptk/reify ::create-version
ptk/WatchEvent
(watch [_ _ _]
(let [label (dt/format (dt/now) :date-full)]
;; Force persist before creating snapshot, otherwise we could loss changes
(rx/concat
(rx/of ::dwp/force-persist)
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :take-file-snapshot {:file-id file-id :label label}))
(rx/mapcat
(fn [{:keys [id]}]
(rx/of
(update-version-state {:editing id})
(fetch-versions file-id))))))))))
(defn rename-version
[file-id id label]
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? id))
(dm/assert! (and (string? label) (d/not-empty? label)))
(ptk/reify ::rename-version
ptk/WatchEvent
(watch [_ _ _]
(rx/merge
(rx/of (update-version-state {:editing false}))
(->> (rp/cmd! :update-file-snapshot {:id id :label label})
(rx/map #(fetch-versions file-id)))))))
(defn restore-version
[project-id file-id id]
(dm/assert! (uuid? project-id))
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? id))
(ptk/reify ::restore-version
ptk/WatchEvent
(watch [_ _ _]
(rx/concat
(rx/of ::dwp/force-persist)
(->> (rx/from-atom refs/persistence-state {:emit-current-value? true})
(rx/filter #(or (nil? %) (= :saved %)))
(rx/take 1)
(rx/mapcat #(rp/cmd! :take-file-snapshot {:file-id file-id :created-by "system" :label (dt/format (dt/now) :date-full)}))
(rx/mapcat #(rp/cmd! :restore-file-snapshot {:file-id file-id :id id}))
(rx/map #(dw/initialize-file project-id file-id)))))))
(defn delete-version
[file-id id]
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? id))
(ptk/reify ::delete-version
ptk/WatchEvent
(watch [_ _ _]
(->> (rp/cmd! :remove-file-snapshot {:id id})
(rx/map #(fetch-versions file-id))))))
(defn pin-version
[file-id id]
(dm/assert! (uuid? file-id))
(dm/assert! (uuid? id))
(ptk/reify ::pin-version
ptk/WatchEvent
(watch [_ state _]
(let [version (->> (dm/get-in state [:workspace-versions :data])
(d/seek #(= (:id %) id)))
params {:id id
:label (dt/format (:created-at version) :date-full)}]
(->> (rp/cmd! :update-file-snapshot params)
(rx/mapcat #(rx/of (update-version-state {:editing id})
(fetch-versions file-id))))))))

View file

@ -21,6 +21,9 @@
[cuerdas.core :as str]
[potok.v2.core :as ptk]))
;; From app.main.data.workspace we can use directly because it causes a circular dependency
(def reload-file nil)
(defn- print-data!
[data]
(-> data
@ -137,6 +140,9 @@
:level :error
:timeout 3000})))
(= code :vern-conflict)
(st/emit! (reload-file))
:else
(st/async-emit! (rt/assign-exception error))))

View file

@ -591,6 +591,9 @@
(def current-file-id
(l/derived :current-file-id st/state))
(def current-project-id
(l/derived :current-project-id st/state))
(def workspace-preview-blend
(l/derived :workspace-preview-blend st/state))
@ -604,4 +607,4 @@
(l/derived :updating-library st/state))
(def persistence-state
(l/derived (comp :status :workspace-persistence) st/state))
(l/derived (comp :status :persistence) st/state))

View file

@ -88,6 +88,7 @@
(def ^:icon-id character-z "character-z")
(def ^:icon-id clip-content "clip-content")
(def ^:icon-id clipboard "clipboard")
(def ^:icon-id clock "clock")
(def ^:icon-id close-small "close-small")
(def ^:icon-id close "close")
(def ^:icon-id code "code")

View file

@ -72,6 +72,7 @@
(def ^:icon bug (icon-xref :bug))
(def ^:icon clip-content (icon-xref :clip-content))
(def ^:icon clipboard (icon-xref :clipboard))
(def ^:icon clock (icon-xref :clock))
(def ^:icon close-small (icon-xref :close-small))
(def ^:icon close (icon-xref :close))
(def ^:icon code (icon-xref :code))

View file

@ -26,6 +26,7 @@
[app.main.ui.workspace.sidebar.options :refer [options-toolbox]]
[app.main.ui.workspace.sidebar.shortcuts :refer [shortcuts-container]]
[app.main.ui.workspace.sidebar.sitemap :refer [sitemap]]
[app.main.ui.workspace.sidebar.versions :refer [versions-toolbox]]
[app.util.debug :as dbg]
[app.util.i18n :refer [tr]]
[rumext.v2 :as mf]))
@ -181,7 +182,17 @@
props
(mf/spread props
:on-change-section handle-change-section
:on-expand handle-expand)]
:on-expand handle-expand)
history-tab
(mf/html
[:article {:class (stl/css :history-tab)}
[:& history-toolbox {}]])
versions-tab
(mf/html
[:article {:class (stl/css :versions-tab)}
[:& versions-toolbox {}]])]
[:& (mf/provider muc/sidebar) {:value :right}
[:aside {:class (stl/css-case :right-settings-bar true
@ -208,7 +219,15 @@
[:& comments-sidebar]
(true? is-history?)
[:> history-toolbox {}]
[:> tab-switcher* {:tabs #js [#js {:label "History" :id "history" :content versions-tab}
#js {:label "Actions" :id "actions" :content history-tab}]
:default-selected "history"
;;:selected (name section)
;;:on-change-tab on-tab-change
:class (stl/css :left-sidebar-tabs)
;;:action-button-position "start"
;;:action-button (mf/html [:& collapse-button {:on-click handle-collapse}])
}]
:else
[:> options-toolbox props])]]]))

View file

@ -109,3 +109,8 @@ $width-settings-bar-max: $s-500;
--collapse-icon-color: var(--color-foreground-primary);
}
}
.versions-tab {
width: 100%;
overflow: hidden;
}

View file

@ -9,12 +9,9 @@
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.main.data.events :as ev]
[app.main.data.workspace :as dw]
[app.main.data.workspace.undo :as dwu]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
[app.util.i18n :refer [tr] :as i18n]
@ -326,18 +323,8 @@
[]
(let [objects (mf/deref refs/workspace-page-objects)
{:keys [items index]} (mf/deref workspace-undo)
entries (parse-entries items objects)
toggle-history
(mf/use-fn
#(st/emit! (-> (dw/toggle-layout-flag :document-history)
(vary-meta assoc ::ev/origin "history-toolbox"))))]
entries (parse-entries items objects)]
[:div {:class (stl/css :history-toolbox)}
[:div {:class (stl/css :history-toolbox-title)}
[:span (tr "workspace.undo.title")]
[:> icon-button* {:variant "ghost"
:aria-label (tr "labels.close")
:on-click toggle-history
:icon "close"}]]
(if (empty? entries)
[:div {:class (stl/css :history-entry-empty)}
[:div {:class (stl/css :history-entry-empty-icon)} i/history]

View file

@ -0,0 +1,377 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License v. 2.0. If a copy of the MPL was not distributed with this
;; file You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.workspace.sidebar.versions
(:require-macros [app.main.style :as stl])
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.time :as ct]
[app.common.uuid :as uuid]
[app.config :as cfg]
[app.main.data.notifications :as ntf]
[app.main.data.workspace.versions :as dwv]
[app.main.refs :as refs]
[app.main.store :as st]
[app.main.ui.components.dropdown :refer [dropdown]]
[app.main.ui.components.select :refer [select]]
[app.main.ui.ds.buttons.icon-button :refer [icon-button*]]
[app.main.ui.ds.foundations.assets.icon :as i]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.keyboard :as kbd]
[app.util.time :as dt]
[cuerdas.core :as str]
[okulary.core :as l]
[rumext.v2 :as mf]))
(def versions
(l/derived :workspace-versions st/state))
(defn group-snapshots
[data]
(->> (concat
(->> data
(filterv #(= "user" (:created-by %)))
(map #(assoc % :type :version)))
(->> data
(filterv #(= "system" (:created-by %)))
(group-by #(.toISODate ^js (:created-at %)))
(map (fn [[day entries]]
{:type :snapshot
:created-at (ct/parse-instant day)
:snapshots entries}))))
(sort-by :created-at)
(reverse)))
(mf/defc version-entry
[{:keys [entry profile on-restore-version on-delete-version on-rename-version editing?]}]
(let [input-ref (mf/use-ref nil)
show-menu? (mf/use-state false)
handle-open-menu
(mf/use-fn
(fn []
(reset! show-menu? true)))
handle-close-menu
(mf/use-fn
(fn []
(reset! show-menu? false)))
handle-rename-version
(mf/use-fn
(mf/deps entry)
(fn []
(st/emit! (dwv/update-version-state {:editing (:id entry)}))))
handle-restore-version
(mf/use-fn
(mf/deps entry on-restore-version)
(fn []
(when on-restore-version
(on-restore-version (:id entry)))))
handle-delete-version
(mf/use-callback
(mf/deps entry on-delete-version)
(fn []
(when on-delete-version
(on-delete-version (:id entry)))))
handle-name-input-focus
(mf/use-fn
(fn [event]
(dom/select-text! (dom/get-target event))))
handle-name-input-blur
(mf/use-fn
(mf/deps entry on-rename-version)
(fn [event]
(let [label (str/trim (dom/get-target-val event))]
(when (and (not (str/empty? label))
(some? on-rename-version))
(on-rename-version (:id entry) label))
(st/emit! (dwv/update-version-state {:editing nil})))))
handle-name-input-key-down
(mf/use-fn
(mf/deps handle-name-input-blur)
(fn [event]
(cond
(kbd/enter? event)
(handle-name-input-blur event)
(kbd/esc? event)
(st/emit! (dwv/update-version-state {:editing nil})))))]
[:li {:class (stl/css :version-entry-wrap)}
[:div {:class (stl/css :version-entry :is-snapshot)}
[:img {:class (stl/css :version-entry-avatar)
:alt (:fullname profile)
:src (cfg/resolve-profile-photo-url profile)}]
[:div {:class (stl/css :version-entry-data)}
(if editing?
[:input {:class (stl/css :version-entry-name-edit)
:type "text"
:ref input-ref
:on-focus handle-name-input-focus
:on-blur handle-name-input-blur
:on-key-down handle-name-input-key-down
:auto-focus true
:default-value (:label entry)}]
[:p {:class (stl/css :version-entry-name)}
(:label entry)])
[:p {:class (stl/css :version-entry-time)}
(let [locale (mf/deref i18n/locale)
time (dt/timeago (:created-at entry) {:locale locale})]
[:span {:class (stl/css :date)} time])]]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.versions.version-menu")
:on-click handle-open-menu
:icon "menu"}]]
[:& dropdown {:show @show-menu? :on-close handle-close-menu}
[:ul {:class (stl/css :version-options-dropdown)}
[:li {:class (stl/css :menu-option)
:role "button"
:on-click handle-rename-version} (tr "labels.rename")]
[:li {:class (stl/css :menu-option)
:role "button"
:on-click handle-restore-version} (tr "labels.restore")]
[:li {:class (stl/css :menu-option)
:role "button"
:on-click handle-delete-version} (tr "labels.delete")]]]]))
(mf/defc snapshot-entry
[{:keys [index is-expanded entry on-toggle-expand on-pin-snapshot on-restore-snapshot]}]
(let [open-menu (mf/use-state nil)
handle-toggle-expand
(mf/use-fn
(mf/deps index on-toggle-expand)
(fn []
(when on-toggle-expand
(on-toggle-expand index))))
handle-pin-snapshot
(mf/use-fn
(mf/deps on-pin-snapshot)
(fn [event]
(let [node (dom/get-current-target event)
id (-> (dom/get-data node "id") uuid/uuid)]
(when on-pin-snapshot (on-pin-snapshot id)))))
handle-restore-snapshot
(mf/use-fn
(mf/deps on-restore-snapshot)
(fn [event]
(let [node (dom/get-current-target event)
id (-> (dom/get-data node "id") uuid/uuid)]
(when on-restore-snapshot (on-restore-snapshot id)))))]
[:li {:class (stl/css :version-entry-wrap)}
[:div {:class (stl/css-case :version-entry true
:is-autosave true
:is-expanded is-expanded)}
[:p {:class (stl/css :version-entry-name)}
(tr "workspace.versions.autosaved.version" (dt/format (:created-at entry) :date-full))]
[:button {:class (stl/css :version-entry-snapshots)
:aria-label (tr "workspace.versions.expand-snapshot")
:on-click handle-toggle-expand}
[:> i/icon* {:id i/clock :class (stl/css :icon-clock)}]
(tr "workspace.versions.autosaved.entry" (count (:snapshots entry)))
[:> i/icon* {:id i/arrow :class (stl/css :icon-arrow)}]]
[:ul {:class (stl/css :version-snapshot-list)}
(for [[idx snapshot] (d/enumerate (:snapshots entry))]
[:li {:class (stl/css :version-snapshot-entry-wrapper)
:key (dm/str "snp-" idx)}
[:div {:class (stl/css :version-snapshot-entry)}
(str
(dt/format (:created-at snapshot) :date-full)
" . "
(dt/format (:created-at snapshot) :time-24-simple))]
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.versions.snapshot-menu")
:on-click #(reset! open-menu snapshot)
:icon "menu"
:class (stl/css :version-snapshot-menu-btn)}]
[:& dropdown {:show (= @open-menu snapshot)
:on-close #(reset! open-menu nil)}
[:ul {:class (stl/css :version-options-dropdown)}
[:li {:class (stl/css :menu-option)
:role "button"
:data-id (dm/str (:id snapshot))
:on-click handle-restore-snapshot}
(tr "workspace.versions.button.restore")]
[:li {:class (stl/css :menu-option)
:role "button"
:data-id (dm/str (:id snapshot))
:on-click handle-pin-snapshot}
(tr "workspace.versions.button.pin")]]]])]]]))
(mf/defc versions-toolbox
[]
(let [users (mf/deref refs/users)
profile (mf/deref refs/profile)
project-id (mf/deref refs/current-project-id)
file-id (mf/deref refs/current-file-id)
expanded (mf/use-state #{})
{:keys [status data editing]}
(mf/deref versions)
;; Store users that have a version
data-users
(mf/use-memo
(mf/deps data)
(fn []
(into #{} (keep (fn [{:keys [created-by profile-id]}]
(when (= "user" created-by) profile-id))) data)))
data
(mf/use-memo
(mf/deps @versions)
(fn []
(->> data
(filter #(or (not (:filter @versions))
(and
(= "user" (:created-by %))
(= (:filter @versions) (:profile-id %)))))
(group-snapshots))))
handle-create-version
(mf/use-fn
(fn []
(st/emit! (dwv/create-version file-id))))
handle-toggle-expand
(mf/use-fn
(fn [id]
(swap! expanded
(fn [expanded]
(let [has-element? (contains? expanded id)]
(cond-> expanded
has-element? (disj id)
(not has-element?) (conj id)))))))
handle-rename-version
(mf/use-fn
(mf/deps file-id)
(fn [id label]
(st/emit! (dwv/rename-version file-id id label))))
handle-restore-version
(mf/use-fn
(mf/deps project-id file-id)
(fn [id]
(st/emit!
(ntf/dialog
:content (tr "workspace.versions.restore-warning")
:controls :inline-actions
:actions [{:label (tr "workspace.updates.dismiss")
:type :secondary
:callback #(st/emit! (ntf/hide))}
{:label (tr "labels.restore")
:type :primary
:callback #(st/emit! (dwv/restore-version project-id file-id id))}]
:tag :restore-dialog))))
handle-delete-version
(mf/use-fn
(mf/deps file-id)
(fn [id]
(st/emit! (dwv/delete-version file-id id))))
handle-pin-version
(mf/use-fn
(mf/deps file-id)
(fn [id]
(st/emit! (dwv/pin-version file-id id))))
handle-change-filter
(mf/use-fn
(fn [filter]
(cond
(= :all filter)
(st/emit! (dwv/update-version-state {:filter nil}))
(= :own filter)
(st/emit! (dwv/update-version-state {:filter (:id profile)}))
:else
(st/emit! (dwv/update-version-state {:filter filter})))))]
(mf/with-effect
[file-id]
(when file-id
(st/emit! (dwv/init-version-state file-id))))
[:div {:class (stl/css :version-toolbox)}
[:& select
{:default-value :all
:aria-label (tr "workspace.versions.filter.label")
:options (into [{:value :all :label (tr "workspace.versions.filter.all")}
{:value :own :label (tr "workspace.versions.filter.mine")}]
(->> data-users
(keep
(fn [id]
(let [{:keys [fullname]} (get users id)]
(when (not= id (:id profile))
{:value id :label (tr "workspace.versions.filter.user" fullname)}))))))
:on-change handle-change-filter}]
(cond
(= status :loading)
[:div {:class (stl/css :versions-entry-empty)}
[:div {:class (stl/css :versions-entry-empty-msg)} (tr "workspace.versions.loading")]]
(= status :loaded)
[:*
[:div {:class (stl/css :version-save-version)}
(tr "workspace.versions.button.save")
[:> icon-button* {:variant "ghost"
:aria-label (tr "workspace.versions.button.save")
:on-click handle-create-version
:icon "pin"}]]
(if (empty? data)
[:div {:class (stl/css :versions-entry-empty)}
[:div {:class (stl/css :versions-entry-empty-icon)} [:> i/icon* {:id i/history}]]
[:div {:class (stl/css :versions-entry-empty-msg)} (tr "workspace.versions.empty")]]
[:ul {:class (stl/css :versions-entries)}
(for [[idx-entry entry] (->> data (map-indexed vector))]
(case (:type entry)
:version
[:& version-entry {:key idx-entry
:entry entry
:editing? (= (:id entry) editing)
:profile (get users (:profile-id entry))
:on-rename-version handle-rename-version
:on-restore-version handle-restore-version
:on-delete-version handle-delete-version}]
:snapshot
[:& snapshot-entry {:key idx-entry
:index idx-entry
:entry entry
:is-expanded (contains? @expanded idx-entry)
:on-toggle-expand handle-toggle-expand
:on-restore-snapshot handle-restore-version
:on-pin-snapshot handle-pin-version}]
nil))])])]))

View file

@ -0,0 +1,226 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@import "refactor/common-refactor.scss";
.version-toolbox {
padding: $s-8;
}
.versions-entry-empty {
align-items: center;
color: var(--color-foreground-secondary);
display: flex;
flex-direction: column;
font-size: $fs-12;
gap: $s-8;
padding: $s-16;
}
.versions-entry-empty-icon {
background: var(--color-background-tertiary);
border-radius: 50%;
padding: $s-8;
display: flex;
}
.version-save-version {
font-weight: 600;
text-transform: uppercase;
color: var(--color-foreground-secondary);
font-size: $fs-12;
padding: $s-16 0 $s-16 $s-16;
justify-content: space-between;
width: 100%;
display: flex;
align-items: center;
}
.version-save-button {
background: none;
border: none;
cursor: pointer;
}
.versions-entries {
display: flex;
flex-direction: column;
gap: $s-6;
}
.version-entry {
border: 1px solid transparent;
p {
margin: 0;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&:hover .version-entry-options {
visibility: initial;
}
}
.version-entry {
display: flex;
padding: $s-4 $s-4 $s-4 $s-16;
gap: $s-8;
border-radius: 8px;
align-items: center;
&:hover {
border-color: var(--color-accent-primary);
}
}
.version-entry.is-autosave {
flex-direction: column;
align-items: start;
padding-left: $s-48;
gap: 0;
}
.version-entry-wrap {
position: relative;
}
.version-entry-avatar {
border-radius: 50%;
width: $s-24;
height: $s-24;
}
.version-entry-data {
width: 100%;
overflow: hidden;
display: flex;
flex-direction: column;
}
.version-entry-name {
color: var(--color-foreground-primary);
border-bottom: 1px solid transparent;
}
.version-entry-name-edit {
font-size: $fs-12;
color: var(--color-foreground-primary);
background: none;
margin: 0;
padding: 0;
border: none;
outline: none;
border-bottom: 1px solid var(--color-foreground-secondary);
}
.version-entry-time {
color: var(--color-foreground-secondary);
}
.version-entry-options {
background: none;
border: 0;
cursor: pointer;
visibility: hidden;
padding: 0;
height: $s-40;
width: $s-32;
}
.version-options-dropdown {
@extend .dropdown-wrapper;
position: absolute;
width: fit-content;
max-width: $s-200;
right: 0;
left: unset;
.menu-option {
@extend .dropdown-element-base;
}
}
.version-entry-snapshots {
display: flex;
align-items: center;
gap: $s-6;
color: var(--color-foreground-secondary);
background: none;
border: 0;
cursor: pointer;
padding: 0;
.icon-clock {
stroke: var(--color-accent-warning);
}
.icon-arrow {
stroke: var(--color-foreground-secondary);
}
&:hover {
color: var(--color-accent-primary);
.icon-arrow {
stroke: var(--color-accent-primary);
}
}
.is-expanded & .icon-arrow {
transform: rotate(90deg);
}
}
.version-snapshot-list {
display: none;
margin-top: $s-8;
flex-direction: column;
width: 100%;
.version-entry.is-expanded & {
display: flex;
}
}
.version-snapshot-entry-wrapper {
display: flex;
justify-content: space-between;
align-items: center;
position: relative;
&:hover .version-snapshot-menu-btn {
visibility: initial;
}
}
.version-snapshot-entry {
font-size: $fs-12;
color: var(--color-foreground-secondary);
background: none;
border: none;
cursor: pointer;
padding: 0;
display: flex;
justify-content: space-between;
width: 100%;
white-space: nowrap;
&:hover {
color: var(--color-accent-primary);
}
&:active {
color: var(--color-accent-primary);
:global(.icon-pin) {
visibility: initial;
fill: var(--color-accent-primary);
}
}
}
.version-snapshot-menu-btn {
visibility: hidden;
}

View file

@ -429,12 +429,12 @@
params {:id (:id file)
:revn (:revn file)
:vern (:vern file)
:session-id sid
:changes changes
:features features
:skip-validate true}]
(->> (rp/cmd! :update-file params)
(rx/subs! (fn [_]
(when reload?