diff --git a/frontend/resources/locales.json b/frontend/resources/locales.json index 589aef343..4c738a553 100644 --- a/frontend/resources/locales.json +++ b/frontend/resources/locales.json @@ -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" : "", diff --git a/frontend/resources/styles/main/partials/sidebar-document-history.scss b/frontend/resources/styles/main/partials/sidebar-document-history.scss index 817290660..ec00f2f73 100644 --- a/frontend/resources/styles/main/partials/sidebar-document-history.scss +++ b/frontend/resources/styles/main/partials/sidebar-document-history.scss @@ -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; + } + } } diff --git a/frontend/src/app/main/data/workspace/selection.cljs b/frontend/src/app/main/data/workspace/selection.cljs index b2a91a917..d1282db64 100644 --- a/frontend/src/app/main/data/workspace/selection.cljs +++ b/frontend/src/app/main/data/workspace/selection.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace/sidebar/history.cljs b/frontend/src/app/main/ui/workspace/sidebar/history.cljs index 5cc931dcf..6f14f6933 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/history.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/history.cljs @@ -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)}])])]))