mirror of
https://github.com/penpot/penpot.git
synced 2026-05-11 19:13:49 +00:00
✨ Add author, relative timestamp and short identifier to history entries (#9132)
The workspace Actions history panel previously showed the operation icon and a one-line message for each undo entry with no indication of when the action happened, any stable way to refer to it, or who made the change. The reporter of issue #7660 (and @Takhoffman's follow-up comment) asked for a git-like display: `<hash> · <time> by <name>`. This change stamps each undo entry with its creation timestamp and author at the moment it lands on the undo stack and surfaces three extra pieces of information in the history sidebar: - A short 7-character identifier derived from the entry's existing `:undo-group` UUID. Hovering shows the full UUID. - A relative timestamp (e.g. `just now`, `5 minutes ago`, `2 hours ago`, `3 days ago`) rendered via `app.common.time/timeago` so it matches the formatting already used for comments and the dashboard. - The display name of the profile that created the entry, rendered as `by <Name>` in the same metadata row. The undo stack is client-side per profile, so every entry is always the current user; the author is stored on the entry anyway so the UI does not need to reach into profile state while rendering and so the data stays correct if the stack shape ever changes. Changes at a glance: - `data/workspace/undo.cljs`: extend `schema:undo-entry` with an optional `:timestamp` and `:by`; new `profile-display-name` helper that falls back from full name to email to nil; `stamp-entry` now takes state and fills in both fields on entries that do not already carry them. Pre-stamped entries (e.g. coming out of an accumulated transaction) keep their original values. - `ui/workspace/sidebar/history.cljs`: propagate `:timestamp`, `:undo-group`, and `:by` through `parse-entries`; add `short-id` helper; render the metadata row in `history-entry` using `app.common.time/timeago` against `:timestamp`; skip the author span entirely when `:by` is nil. - `ui/workspace/sidebar/history.scss`: styling for the new metadata row (monospace hash, muted separator, truncated time/author). - `translations/en.po`: 1 new string for `by %s`. Existing undo entries created before this change have neither timestamp nor author; the UI is defensive about both, so old entries simply render with whatever data they have (and often the plain title on its own). Github #7660 Signed-off-by: FairyPigDev <luislee3108@gmail.com> Signed-off-by: FairyPiggyDev <luislee3108@gmail.com> Co-authored-by: Andrey Antukh <niwi@niwi.nz>
This commit is contained in:
parent
f7fbd3007e
commit
09bd7f96f6
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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 <Name>") 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?)
|
||||
|
||||
@ -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 <name>") — 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 {
|
||||
|
||||
@ -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"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user