diff --git a/CHANGES.md b/CHANGES.md index 2824a44242..06f18a6d08 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -33,6 +33,8 @@ - Enhance readability of applied tokens in plugins API [Taiga #13714](https://tree.taiga.io/project/penpot/issue/13714) - Add "Delete group" option to the assets panel context menu for components, colors and typographies (by @FairyPigDev) [Github #9141](https://github.com/penpot/penpot/issues/9141) +- Add `Alt+click` on a layer's disclosure arrow to recursively expand the entire subtree rooted at that layer in the Layers sidebar; symmetric with the existing `Shift+click` collapse-all gesture, and removes the O(siblings × depth) click cost of unfolding a deep subtree one level at a time [Github #7736](https://github.com/penpot/penpot/issues/7736) +- Show relative timestamp and short identifier for each entry in the workspace Actions history sidebar (by @FairyPigDev) [Github #7660](https://github.com/penpot/penpot/issues/7660) - Add `Alt+click` on a layer's disclosure arrow to recursively expand the entire subtree in the Layers sidebar (by @MilosM348) [Github #9179](https://github.com/penpot/penpot/pull/9179) - Show alpha percentage next to library color values to distinguish colors that differ only in opacity (by @rockchris099) [Github #6328](https://github.com/penpot/penpot/issues/6328) - Add "Clear artboard guides" option to right-click context menu for frames (by @eureka0928) [Github #6987](https://github.com/penpot/penpot/issues/6987) diff --git a/frontend/src/app/main/data/workspace/undo.cljs b/frontend/src/app/main/data/workspace/undo.cljs index 2296aed447..c8e7c678bd 100644 --- a/frontend/src/app/main/data/workspace/undo.cljs +++ b/frontend/src/app/main/data/workspace/undo.cljs @@ -62,7 +62,16 @@ [:undo-group ::sm/uuid] [:tags [:set :keyword]] [:selected-before {:optional true} [:maybe [:set ::sm/uuid]]] - [:selected-after {:optional true} [:maybe [:set ::sm/uuid]]]]) + [:selected-after {:optional true} [:maybe [:set ::sm/uuid]]] + ;; When the entry was pushed onto the stack; used by the actions + ;; history panel to show a relative timestamp next to each entry. + ;; Issue #7660. + [:timestamp {:optional true} [:maybe some?]] + ;; Display name of the profile that created the entry. The undo + ;; stack is client-side per profile, so this is always the current + ;; user; still stored explicitly so the UI does not need to reach + ;; into profile state while rendering. Issue #7660. + [:by {:optional true} [:maybe :string]]]) (def check-undo-entry (sm/check-fn schema:undo-entry)) @@ -87,6 +96,26 @@ (update [_ state] (update state :workspace-undo assoc :index index)))) +(defn- profile-display-name + "Best-effort display name for the current profile. Prefers the full + name, falls back to the email, and finally to nil so the UI can + simply skip the 'by …' suffix when we have nothing useful to show. + Issue #7660." + [state] + (let [profile (get state :profile)] + (or (:fullname profile) + (:email profile)))) + +(defn- stamp-entry + "Attach creation metadata to an undo entry. We only stamp the timestamp + and author when they are missing so already-enriched entries (e.g. + coming from an accumulated transaction that was opened earlier) keep + their original creation time and attribution. Issue #7660." + [state entry] + (cond-> entry + (nil? (:timestamp entry)) (assoc :timestamp (ct/now)) + (nil? (:by entry)) (assoc :by (profile-display-name state)))) + (defn- add-undo-entry [state entry] (if (and entry @@ -95,7 +124,7 @@ (let [index (get-in state [:workspace-undo :index] -1) items (get-in state [:workspace-undo :items] []) items (->> items (take (inc index)) (into [])) - items (conj-undo-entry items entry)] + items (conj-undo-entry items (stamp-entry state entry))] (-> state (update :workspace-undo assoc :items items :index (min (inc index) @@ -104,7 +133,9 @@ (defn- stack-undo-entry "Extends the current undo entry in the workspace with new changes if it - exists, or creates a new entry if it doesn't." + exists, or creates a new entry if it doesn't. When stacking onto an + existing entry, the entry's original timestamp is preserved so the + history panel keeps showing when the action originated. Issue #7660." [state {:keys [undo-changes redo-changes selected-after] :as entry}] (let [index (get-in state [:workspace-undo :index] -1)] (if (>= index 0) diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.cljs b/frontend/src/app/main/ui/workspace/sidebar/history.cljs index 366fc1715e..eae0f0dcea 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/history.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/history.cljs @@ -9,6 +9,7 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] + [app.common.time :as ct] [app.main.data.workspace.undo :as dwu] [app.main.refs :as refs] [app.main.store :as st] @@ -172,6 +173,17 @@ (->> redo-changes (map parse-change))) +(defn- short-id + "Build a short git-like label for an undo entry. Derives it from the + entry's `:undo-group` uuid (which the undo subsystem already generates + for every action). Issue #7660." + [entry] + (when-let [group (:undo-group entry)] + (let [s (str group)] + (-> s + (str/replace #"[^0-9a-f]" "") + (subs 0 (min 7 (count s))))))) + (defn safe-name [maybe-keyword] (if (keyword? maybe-keyword) (name maybe-keyword) @@ -253,10 +265,17 @@ (assoc selected-entry :detail detail))) (defn parse-entries [entries objects] - (->> entries - (map parse-entry) - (map (resolve-shape-types entries objects)) - (mapv select-entry))) + ;; Propagate per-entry metadata (timestamp, undo-group, author) onto + ;; the summarized result so the UI can show when the action happened, + ;; a short stable identifier, and who made the change. Issue #7660. + (mapv (fn [raw-entry] + (-> (parse-entry raw-entry) + ((resolve-shape-types entries objects)) + (select-entry) + (assoc :timestamp (:timestamp raw-entry) + :undo-group (:undo-group raw-entry) + :by (:by raw-entry)))) + entries)) (mf/defc history-entry-details* [{:keys [entry]}] (let [{entries :items} (mf/deref workspace-undo) @@ -289,6 +308,11 @@ [{:keys [entry idx-entry disabled? current?]}] (let [hover? (mf/use-state false) show-detail? (mf/use-state false) + + relative-time (ct/timeago (:timestamp entry)) + label (short-id entry) + author (:by entry) + toggle-show-detail (mf/use-fn (fn [event] @@ -310,7 +334,28 @@ [:div {:class (stl/css :history-entry-summary)} [:div {:class (stl/css :history-entry-summary-icon)} (entry->icon entry)] - [:div {:class (stl/css :history-entry-summary-text)} (entry->message entry)] + [:div {:class (stl/css :history-entry-summary-text)} + [:div {:class (stl/css :history-entry-title)} + (entry->message entry)] + ;; Metadata row: short identifier, relative timestamp, and + ;; author. Rendered as plain inline text so the natural word + ;; spacing between tokens ("17bc430 · just now by ") is + ;; preserved without relying on flex gap. Issue #7660. + (when (or label relative-time author) + [:div {:class (stl/css :history-entry-meta)} + (when label + [:span {:class (stl/css :history-entry-hash) + :title (dm/str (:undo-group entry))} + label]) + (when (and label relative-time) + [:span {:class (stl/css :history-entry-meta-sep)} " · "]) + (when relative-time + [:span {:class (stl/css :history-entry-time)} + relative-time]) + (when (and (or label relative-time) author) " ") + (when author + [:span {:class (stl/css :history-entry-author)} + (tr "workspace.undo.entry.by" author)])])] (when (:detail entry) [:div {:class (stl/css-case :history-entry-summary-button true :button-opened @show-detail?) diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.scss b/frontend/src/app/main/ui/workspace/sidebar/history.scss index 069d1d5d73..7af8eaad1d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/history.scss +++ b/frontend/src/app/main/ui/workspace/sidebar/history.scss @@ -54,8 +54,49 @@ } .history-entry-summary-text { + display: flex; + flex-direction: column; + gap: deprecated.$s-2; margin: 0 deprecated.$s-8; color: var(--color-foreground-primary); + min-inline-size: 0; + } + + .history-entry-title { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + // Metadata row: short identifier, relative timestamp, and author. + // Plain inline layout so the literal spaces embedded in the + // template ("hash · time by Name") survive rendering. Issue #7660. + .history-entry-meta { + color: var(--color-foreground-secondary); + font-size: deprecated.$fs-10; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + .history-entry-hash { + font-family: monospace; + letter-spacing: 0.03em; + } + + .history-entry-meta-sep { + opacity: 0.6; + } + + .history-entry-time { + white-space: nowrap; + } + + // Author ("by ") — the surrounding meta row already + // truncates, so keep the author span as a plain inline. Issue + // #7660. + .history-entry-author { + white-space: nowrap; } .history-entry-summary-button { diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 82c552a983..32f61bbaf1 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -9014,6 +9014,10 @@ msgstr "Refresh" msgid "workspace.undo.empty" msgstr "There are no history changes so far" +#: src/app/main/ui/workspace/sidebar/history.cljs +msgid "workspace.undo.entry.by" +msgstr "by %s" + #: src/app/main/ui/workspace/sidebar/history.cljs:147 msgid "workspace.undo.entry.delete" msgstr "Deleted %s"