0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 02:28:18 -05:00

Visual redesign for undo history

This commit is contained in:
alonso.torres 2020-10-19 15:42:56 +02:00 committed by Hirunatan
parent 0e3c3ebfbd
commit b8e47c87ba
4 changed files with 637 additions and 97 deletions

View file

@ -1441,7 +1441,7 @@
}
},
"settings.multiple" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:153", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:163", "src/app/main/ui/workspace/sidebar/options/typography.cljs:99", "src/app/main/ui/workspace/sidebar/options/typography.cljs:149", "src/app/main/ui/workspace/sidebar/options/typography.cljs:162", "src/app/main/ui/workspace/sidebar/options/stroke.cljs:156" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:153", "src/app/main/ui/workspace/sidebar/options/rows/color_row.cljs:163", "src/app/main/ui/workspace/sidebar/options/typography.cljs:99", "src/app/main/ui/workspace/sidebar/options/typography.cljs:149", "src/app/main/ui/workspace/sidebar/options/typography.cljs:162", "src/app/main/ui/workspace/sidebar/options/stroke.cljs:147" ],
"translations" : {
"en" : "Mixed",
"fr" : null,
@ -1666,7 +1666,7 @@
}
},
"workspace.assets.assets" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:629" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:628" ],
"translations" : {
"en" : "Assets",
"fr" : "",
@ -1675,7 +1675,7 @@
}
},
"workspace.assets.box-filter-all" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:649" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:648" ],
"translations" : {
"en" : "All assets",
"fr" : "",
@ -1702,7 +1702,7 @@
"unused" : true
},
"workspace.assets.colors" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:329", "src/app/main/ui/workspace/sidebar/assets.cljs:652" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:329", "src/app/main/ui/workspace/sidebar/assets.cljs:651" ],
"translations" : {
"en" : "Colors",
"fr" : "",
@ -1711,7 +1711,7 @@
}
},
"workspace.assets.components" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:83", "src/app/main/ui/workspace/sidebar/assets.cljs:650" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:83", "src/app/main/ui/workspace/sidebar/assets.cljs:649" ],
"translations" : {
"en" : "Components",
"fr" : "",
@ -1738,7 +1738,7 @@
}
},
"workspace.assets.file-library" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:532" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:531" ],
"translations" : {
"en" : "File library",
"fr" : "",
@ -1747,7 +1747,7 @@
}
},
"workspace.assets.graphics" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:164", "src/app/main/ui/workspace/sidebar/assets.cljs:651" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:164", "src/app/main/ui/workspace/sidebar/assets.cljs:650" ],
"translations" : {
"en" : "Graphics",
"fr" : "",
@ -1756,7 +1756,7 @@
}
},
"workspace.assets.libraries" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:632" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:631" ],
"translations" : {
"en" : "Libraries",
"fr" : "",
@ -1765,7 +1765,7 @@
}
},
"workspace.assets.not-found" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:593" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:592" ],
"translations" : {
"en" : "No assets found",
"fr" : "",
@ -1783,7 +1783,7 @@
}
},
"workspace.assets.search" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:636" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:635" ],
"translations" : {
"en" : "Search assets",
"fr" : "",
@ -1792,7 +1792,7 @@
}
},
"workspace.assets.shared" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:534" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:533" ],
"translations" : {
"en" : "SHARED",
"fr" : "",
@ -1801,7 +1801,7 @@
}
},
"workspace.assets.typography" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:420", "src/app/main/ui/workspace/sidebar/assets.cljs:653" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/assets.cljs:420", "src/app/main/ui/workspace/sidebar/assets.cljs:652" ],
"translations" : {
"en" : "Typographies"
}
@ -1855,13 +1855,13 @@
}
},
"workspace.gradients.linear" : {
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:39", "src/app/main/ui/components/color_bullet.cljs:30" ],
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:39", "src/app/main/ui/components/color_bullet.cljs:31" ],
"translations" : {
"en" : "Linear gradient"
}
},
"workspace.gradients.radial" : {
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:40", "src/app/main/ui/components/color_bullet.cljs:31" ],
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:40", "src/app/main/ui/components/color_bullet.cljs:32" ],
"translations" : {
"en" : "Radial gradient"
}
@ -2044,19 +2044,19 @@
}
},
"workspace.libraries.colors.big-thumbnails" : {
"used-in" : [ "src/app/main/ui/workspace/colorpalette.cljs:171" ],
"used-in" : [ "src/app/main/ui/workspace/colorpalette.cljs:169" ],
"translations" : {
"en" : "Big thumbnails"
}
},
"workspace.libraries.colors.file-library" : {
"used-in" : [ "src/app/main/ui/workspace/colorpicker/libraries.cljs:87", "src/app/main/ui/workspace/colorpalette.cljs:149" ],
"used-in" : [ "src/app/main/ui/workspace/colorpicker/libraries.cljs:87", "src/app/main/ui/workspace/colorpalette.cljs:147" ],
"translations" : {
"en" : "File library"
}
},
"workspace.libraries.colors.recent-colors" : {
"used-in" : [ "src/app/main/ui/workspace/colorpicker/libraries.cljs:86", "src/app/main/ui/workspace/colorpalette.cljs:159" ],
"used-in" : [ "src/app/main/ui/workspace/colorpicker/libraries.cljs:86", "src/app/main/ui/workspace/colorpalette.cljs:157" ],
"translations" : {
"en" : "Recent colors"
}
@ -2068,7 +2068,7 @@
}
},
"workspace.libraries.colors.small-thumbnails" : {
"used-in" : [ "src/app/main/ui/workspace/colorpalette.cljs:176" ],
"used-in" : [ "src/app/main/ui/workspace/colorpalette.cljs:174" ],
"translations" : {
"en" : "Small thumbnails"
}
@ -2173,13 +2173,13 @@
}
},
"workspace.libraries.text.multiple-typography" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:266" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:267" ],
"translations" : {
"en" : "Multiple typographies"
}
},
"workspace.libraries.text.multiple-typography-tooltip" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:268" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/text.cljs:269" ],
"translations" : {
"en" : "Unlink all typographies"
}
@ -2302,7 +2302,7 @@
}
},
"workspace.options.fill" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:54" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:53" ],
"translations" : {
"en" : "Fill",
"fr" : "Remplissage",
@ -2500,7 +2500,7 @@
}
},
"workspace.options.group-fill" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:53" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:52" ],
"translations" : {
"en" : "Group fill",
"fr" : null,
@ -2509,7 +2509,7 @@
}
},
"workspace.options.group-stroke" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:72" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:63" ],
"translations" : {
"en" : "Group stroke",
"fr" : null,
@ -2590,7 +2590,7 @@
}
},
"workspace.options.selection-fill" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:52" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/fill.cljs:51" ],
"translations" : {
"en" : "Selection fill",
"fr" : null,
@ -2599,7 +2599,7 @@
}
},
"workspace.options.selection-stroke" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:71" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:62" ],
"translations" : {
"en" : "Selection stroke",
"fr" : null,
@ -2668,7 +2668,7 @@
}
},
"workspace.options.stroke" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:73" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:64" ],
"translations" : {
"en" : "Stroke",
"fr" : "Bordure",
@ -2677,7 +2677,7 @@
}
},
"workspace.options.stroke.center" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:163" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:154" ],
"translations" : {
"en" : "Center",
"fr" : "Centre",
@ -2686,7 +2686,7 @@
}
},
"workspace.options.stroke.dashed" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:173" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:164" ],
"translations" : {
"en" : "Dashed",
"fr" : "Tiré",
@ -2695,7 +2695,7 @@
}
},
"workspace.options.stroke.dotted" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:172" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:163" ],
"translations" : {
"en" : "Dotted",
"fr" : "Pointillé",
@ -2704,7 +2704,7 @@
}
},
"workspace.options.stroke.inner" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:164" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:155" ],
"translations" : {
"en" : "Inside",
"fr" : "Intérieur",
@ -2713,7 +2713,7 @@
}
},
"workspace.options.stroke.mixed" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:174" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:165" ],
"translations" : {
"en" : "Mixed",
"fr" : "Mixte",
@ -2722,7 +2722,7 @@
}
},
"workspace.options.stroke.outer" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:165" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:156" ],
"translations" : {
"en" : "Outside",
"fr" : "Extérieur",
@ -2731,7 +2731,7 @@
}
},
"workspace.options.stroke.solid" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:171" ],
"used-in" : [ "src/app/main/ui/workspace/sidebar/options/stroke.cljs:162" ],
"translations" : {
"en" : "Solid",
"fr" : "Solide",
@ -3070,8 +3070,230 @@
"es" : "Texto (T)"
}
},
"workspace.undo.empty" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:272" ],
"translations" : {
"en" : "There are no history changes so far"
}
},
"workspace.undo.entry.delete" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:110" ],
"translations" : {
"en" : "Deleted %s"
}
},
"workspace.undo.entry.modify" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:109" ],
"translations" : {
"en" : "Modified %s"
}
},
"workspace.undo.entry.move" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:111" ],
"translations" : {
"en" : "Moved objects"
}
},
"workspace.undo.entry.multiple.circle" : {
"translations" : {
"en" : "circles"
},
"unused" : true
},
"workspace.undo.entry.multiple.color" : {
"translations" : {
"en" : "color assets"
},
"unused" : true
},
"workspace.undo.entry.multiple.component" : {
"translations" : {
"en" : "components"
},
"unused" : true
},
"workspace.undo.entry.multiple.curve" : {
"translations" : {
"en" : "curves"
},
"unused" : true
},
"workspace.undo.entry.multiple.frame" : {
"translations" : {
"en" : "artboard"
},
"unused" : true
},
"workspace.undo.entry.multiple.group" : {
"translations" : {
"en" : "groups"
},
"unused" : true
},
"workspace.undo.entry.multiple.image" : {
"translations" : {
"en" : "images"
},
"unused" : true
},
"workspace.undo.entry.multiple.media" : {
"translations" : {
"en" : "graphic assets"
},
"unused" : true
},
"workspace.undo.entry.multiple.multiple" : {
"translations" : {
"en" : "objects"
},
"unused" : true
},
"workspace.undo.entry.multiple.page" : {
"translations" : {
"en" : "pages"
},
"unused" : true
},
"workspace.undo.entry.multiple.path" : {
"translations" : {
"en" : "paths"
},
"unused" : true
},
"workspace.undo.entry.multiple.rect" : {
"translations" : {
"en" : "rectangles"
},
"unused" : true
},
"workspace.undo.entry.multiple.shape" : {
"translations" : {
"en" : "shapes"
},
"unused" : true
},
"workspace.undo.entry.multiple.text" : {
"translations" : {
"en" : "texts"
},
"unused" : true
},
"workspace.undo.entry.multiple.typography" : {
"translations" : {
"en" : "typography assets"
},
"unused" : true
},
"workspace.undo.entry.new" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:108" ],
"translations" : {
"en" : "New %s"
}
},
"workspace.undo.entry.single.circle" : {
"translations" : {
"en" : "circle"
},
"unused" : true
},
"workspace.undo.entry.single.color" : {
"translations" : {
"en" : "color asset"
},
"unused" : true
},
"workspace.undo.entry.single.component" : {
"translations" : {
"en" : "component"
},
"unused" : true
},
"workspace.undo.entry.single.curve" : {
"translations" : {
"en" : "curve"
},
"unused" : true
},
"workspace.undo.entry.single.frame" : {
"translations" : {
"en" : "frame"
},
"unused" : true
},
"workspace.undo.entry.single.group" : {
"translations" : {
"en" : "group"
},
"unused" : true
},
"workspace.undo.entry.single.image" : {
"translations" : {
"en" : "image"
},
"unused" : true
},
"workspace.undo.entry.single.media" : {
"translations" : {
"en" : "graphic asset"
},
"unused" : true
},
"workspace.undo.entry.single.multiple" : {
"translations" : {
"en" : "object"
},
"unused" : true
},
"workspace.undo.entry.single.page" : {
"translations" : {
"en" : "page"
},
"unused" : true
},
"workspace.undo.entry.single.path" : {
"translations" : {
"en" : "path"
},
"unused" : true
},
"workspace.undo.entry.single.rect" : {
"translations" : {
"en" : "rectangle"
},
"unused" : true
},
"workspace.undo.entry.single.shape" : {
"translations" : {
"en" : "shape"
},
"unused" : true
},
"workspace.undo.entry.single.text" : {
"translations" : {
"en" : "text"
},
"unused" : true
},
"workspace.undo.entry.single.typography" : {
"translations" : {
"en" : "typography asset"
},
"unused" : true
},
"workspace.undo.entry.unknown" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:112" ],
"translations" : {
"en" : "Operation over %s"
}
},
"workspace.undo.title" : {
"used-in" : [ "src/app/main/ui/workspace/sidebar/history.cljs:268" ],
"translations" : {
"en" : "History"
}
},
"workspace.updates.dismiss" : {
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:538" ],
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:542" ],
"translations" : {
"en" : "Dismiss",
"fr" : "",
@ -3080,7 +3302,7 @@
}
},
"workspace.updates.there-are-updates" : {
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:534" ],
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:538" ],
"translations" : {
"en" : "There are updates in shared libraries",
"fr" : "",
@ -3089,7 +3311,7 @@
}
},
"workspace.updates.update" : {
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:536" ],
"used-in" : [ "src/app/main/data/workspace/libraries.cljs:540" ],
"translations" : {
"en" : "Update",
"fr" : "",

View file

@ -8,37 +8,122 @@
// Copyright (c) 2020 UXBOX Labs SL
.history-toolbox {
display: flex;
flex-direction: column;
display: flex;
flex-direction: column;
}
.history-toolbox-title {
color: $color-gray-10;
font-size: $fs14;
padding: 0.5rem;
color: $color-gray-10;
font-size: $fs14;
padding: 0.5rem;
}
.undo-history {
.history-entry-empty {
display: flex;
flex-direction: column;
align-items: center;
padding: 1rem;
.history-entry-empty-icon {
margin-bottom: 1rem;
svg {
width: 32px;
height: 32px;
fill: $color-gray-40;
}
}
.history-entry-empty-msg {
color: $color-gray-30;
font-size: $fs12;
color: $color-gray-10;
.undo-entry {
max-height: 10rem;
overflow: auto;
margin: 0.5rem;
&.transaction {
border: 2px solid $color-primary;
}
}
.undo-entry-change {
background-color: #1F1F1F;
padding: 0.5rem;
}
.separator {
margin: 0.5rem;
border-color: $color-primary;
}
}
}
.history-entries {
font-size: $fs12;
color: $color-gray-20;
fill: $color-gray-20;
}
.history-entry {
border: 1px solid $color-gray-60;
border-radius: 4px;
margin: 0.5rem;
display: flex;
flex-direction: column;
padding: 0.5rem;
cursor: pointer;
transition: border 0.2s;
&.disabled {
opacity: 0.5;
}
&.current {
background-color: $color-gray-60;
}
&.hover {
border-color: $color-primary;
}
}
.history-entry-summary {
display: flex;
flex-direction: row;
align-items: center;
* {
display: flex;
}
}
.history-entry-summary-icon {
svg {
width: 16px;
height: 16px;
}
}
.history-entry-summary-text {
flex: 1;
margin: 0 0.5rem;
margin-top: 2px;
}
.history-entry-summary-button {
opacity: 0;
transition: transform 0.2s;
svg {
width: 12px;
height: 12px;
}
.show-detail &,
.hover & {
opacity: 1;
}
.show-detail & {
transform: rotate(90deg);
}
}
.history-entry-detail {
display: none;
.show-detail & {
display: block;
padding: 1rem 0 0.5rem 0;
}
.history-entry-details-list {
margin: 0;
li {
margin-bottom: 0.5rem;
}
}
}

View file

@ -244,7 +244,10 @@
:page-id page-id
:parent-id parent-id
:shapes shapes
:index index-in-parent}]
:index index-in-parent}
{:type :del-obj
:page-id page-id
:id (:id group)}]
uchanges [{:type :add-obj
:page-id page-id
:id (:id group)

View file

@ -11,6 +11,7 @@
(:require
[rumext.alpha :as mf]
[cuerdas.core :as str]
[app.common.data :as d]
[app.main.ui.icons :as i]
[app.main.data.history :as udh]
[app.main.data.workspace :as dw]
@ -27,42 +28,271 @@
(def workspace-undo
(l/derived :workspace-undo st/state))
(mf/defc undo-entry [{:keys [index entry objects is-transaction?] :or {is-transaction? false}}]
(let [{:keys [redo-changes]} entry]
[:li.undo-entry {:class (when is-transaction? "transaction")}
(for [[idx-change {:keys [type id operations]}] (map-indexed vector redo-changes)]
[:div.undo-entry-change {:key (str "change-" idx-change)}
[:div.undo-entry-change-data (when type (str type)) " " (when id (str (get-in objects [id :name] (subs (str id) 0 8))))]
(when operations
[:div.undo-entry-change-data (str/join ", "
(map (comp name :attr)
(filter #(= (:type %) :set) operations)))])])]))
(defn get-object
"Searchs for a shape inside the objects list or inside the undo history"
[id entries objects]
(let [search-deleted-shape
(fn [id entries]
(let [search-obj (fn [obj] (and (= (:type obj) :add-obj)
(= (:id obj) id)))
search-delete-entry (fn [{:keys [undo-changes redo-changes]}]
(or (d/seek search-obj undo-changes)
(d/seek search-obj redo-changes)))
{:keys [obj]} (->> entries (d/seek search-delete-entry) search-delete-entry)]
obj))]
(or (get objects id)
(search-deleted-shape id entries))))
(defn extract-operation
"Generalizes the type of operation for different types of change"
[change]
(case (:type change)
(:add-obj :add-page :add-color :add-media :add-component :add-typography) :new
(:mod-obj :mod-page :mod-color :mod-media :mod-component :mod-typography) :modify
(:del-obj :del-page :del-color :del-media :del-component :del-typography) :delete
:mov-objects :move
nil))
(defn parse-change
"Given a single change parses the information into an uniform map"
[change]
(let [r (fn [type id]
{:type type
:operation (extract-operation change)
:detail (:operations change)
:id (cond
(and (coll? id) (= 1 (count id))) (first id)
(coll? id) :multiple
:else id)})]
(case (:type change)
:set-option (r :page (:page-id change))
(:add-obj
:mod-obj
:del-obj) (r :shape (:id change))
:reg-objects nil
:mov-objects (r :shape (:shapes change))
(:add-page
:mod-page :del-page
:mov-page) (r :page (:id change))
(:add-color
:mod-color) (r :color (get-in change [:color :id]))
:del-color (r :color (:id change))
:add-recent-color nil
(:add-media
:mod-media) (r :media (get-in change [:object :id]))
:del-media (r :media (:id change))
(:add-component
:mod-component
:del-component) (r :component (:id change))
(:add-typography
:mod-typography) (r :typography (get-in change [:typography :id]))
:del-typography (r :typography (:id change))
nil)))
(defn resolve-shape-types
"Retrieve the type to be shown to the user"
[entries objects]
(let [resolve-type (fn [{:keys [type id]}]
(if (or (not= type :shape) (= id :multiple))
type
(:type (get-object id entries objects))))
map-fn (fn [entry]
(if (and (= (:type entry) :shape)
(not= (:id entry) :multiple))
(assoc entry :type (resolve-type entry))
entry))]
(fn [entries]
(map map-fn entries))))
(defn entry-type->message
"Formats the message that will be displayed to the user"
[locale type multiple?]
(let [arity (if multiple? "multiple" "single")
attribute (name (or type :multiple))]
(t locale (str/format "workspace.undo.entry.%s.%s" arity attribute))))
(defn entry->message [locale entry]
(let [value (entry-type->message locale (:type entry) (= :multiple (:id entry)))]
(case (:operation entry)
:new (t locale "workspace.undo.entry.new" value)
:modify (t locale "workspace.undo.entry.modify" value)
:delete (t locale "workspace.undo.entry.delete" value)
:move (t locale "workspace.undo.entry.move" value)
(t locale "workspace.undo.entry.unknown" value))))
(defn entry->icon [{:keys [type]}]
(case type
:page i/file-html
:shape i/layers
:rect i/box
:circle i/circle
:text i/text
:curve i/curve
:path i/curve
:frame i/artboard
:group i/folder
:color i/palette
:typography i/titlecase
:component i/component
:media i/image
:image i/image
i/layers))
(defn is-shape? [type]
#{:shape :rect :circle :text :curve :path :frame :group})
(defn parse-entry [{:keys [redo-changes]}]
(->> redo-changes
(map parse-change)))
(defn safe-name [maybe-keyword]
(if (keyword? maybe-keyword)
(name maybe-keyword)
maybe-keyword))
(defn select-entry
"Selects the entry the user will see inside a list of posible entries.
Sometimes the result will be a combination."
[candidates]
(let [;; Group by id and type
entries (->> candidates
(remove nil?)
(group-by #(vector (:type %) (:operation %) (:id %)) ))
single? (fn [coll] (= (count coll) 1))
;; Retrieve also by-type and by-operation
types (group-by first (keys entries))
operations (group-by second (keys entries))
;; The cases for the selection of the representative entry are a bit
;; convoluted. Best to read the comments to clarify.
;; At this stage we have cleaned the entries but we can have a batch
;; of operations for a single undo-entry. We want to select the
;; one that is most interesting for the user.
selected-entry
(cond
;; If we only have one operation over one shape we return the last change
(single? entries)
(-> entries (get (first (keys entries))) (last))
;; If we're creating an object it will have priority
(single? (:new operations))
(-> entries (get (first (:new operations))) (last))
;; If there is only a deletion of 1 group we retrieve this operation because
;; the others will be the children
(single? (filter #(= :group (first %)) (:delete operations)))
(-> entries (get (first (filter #(= :group (first %)) (:delete operations)))) (last))
;; Otherwise we could have the same operation between several
;; types (i.e: delete various shapes). If that happens we return
;; the operation with `:multiple` id
(single? operations)
{:type (if (every? is-shape? (keys types)) :shape :multiple)
:id :multiple
:operation (first (keys operations))}
;; Finally, if we have several operations over several shapes we return
;; `:multiple` for operation and type and join the last of the operations for
;; each shape
:else
{:type :multiple
:id :multiple
:operation :multiple})
;; We add to the detail the information depending on the type of operation
detail
(case (:operation selected-entry)
:new (:id selected-entry)
:modify (->> candidates
(filter #(= :modify (:operation %)))
(group-by :id)
(d/mapm (fn [k v] (->> v
(mapcat :detail)
(map (comp safe-name :attr))
(remove nil?)
(into #{})))))
:delete (->> candidates
(filter #(= :delete (:operation %)))
(map :id))
candidates)]
(assoc selected-entry :detail detail)))
(defn parse-entries [entries objects]
(->> entries
(map parse-entry)
(map (resolve-shape-types entries objects))
(mapv select-entry)))
(mf/defc history-entry-details [{:keys [entry]}]
(let [{entries :items} (mf/deref workspace-undo)
objects (mf/deref refs/workspace-page-objects)]
[:div.history-entry-detail
(case (:operation entry)
:new
(:name (get-object (:detail entry) entries objects))
:delete
[:ul.history-entry-details-list
(for [id (:detail entry)]
(let [shape-name (:name (get-object id entries objects))]
[:li {:key id} shape-name]))]
:modify
[:ul.history-entry-details-list
(for [[id attributes] (:detail entry)]
(let [shape-name (:name (get-object id entries objects))]
[:li {:key id}
[:div shape-name]
[:div (str/join ", " attributes)]]))]
nil)]))
(mf/defc history-entry [{:keys [locale entry disabled? current?]}]
(let [hover? (mf/use-state false)
show-detail? (mf/use-state false)]
[:div.history-entry {:class (dom/classnames
:disabled disabled?
:current current?
:hover @hover?
:show-detail @show-detail?)
:on-mouse-enter #(reset! hover? true)
:on-mouse-leave #(reset! hover? false)
:on-click #(when (:detail entry)
(swap! show-detail? not))
}
[:div.history-entry-summary
[:div.history-entry-summary-icon (entry->icon entry)]
[:div.history-entry-summary-text (entry->message locale entry)]
(when (:detail entry)
[:div.history-entry-summary-button i/arrow-slide])]
(when show-detail?
[:& history-entry-details {:entry entry}])]))
(mf/defc history-toolbox []
(let [locale (mf/deref i18n/locale)
objects (mf/deref refs/workspace-page-objects)
{:keys [items index transaction]} (mf/deref workspace-undo)
objects (mf/deref refs/workspace-page-objects)]
entries (parse-entries items objects)]
[:div.history-toolbox
[:div.history-toolbox-title "History"]
[:ul.undo-history
[:*
(when (and
(> (count items) 0)
(or (nil? index)
(>= index (count items))))
[:hr.separator])
(when transaction
[:& undo-entry {:key (str "transaction")
:objects objects
:is-transaction? true
:entry transaction}])
(for [[idx-entry entry] (->> items (map-indexed vector) reverse)]
[:*
(when (= index idx-entry) [:hr.separator {:data-index index}])
[:& undo-entry {:key (str "entry-" idx-entry)
:objects objects
:entry entry}]])
(when (= index -1) [:hr.separator])]]]))
[:div.history-toolbox-title (t locale "workspace.undo.title")]
(if (empty? entries)
[:div.history-entry-empty
[:div.history-entry-empty-icon i/undo-history]
[:div.history-entry-empty-msg (t locale "workspace.undo.empty")]]
[:ul.history-entries
(for [[idx-entry entry] (->> entries (map-indexed vector) reverse)] #_[i (range 0 10)]
[:& history-entry {:key (str "entry-" idx-entry)
:locale locale
:entry entry
:current? (= idx-entry index)
:disabled? (> idx-entry index)}])])]))