diff --git a/common/src/app/common/data.cljc b/common/src/app/common/data.cljc index 2d0011be7..89687c7aa 100644 --- a/common/src/app/common/data.cljc +++ b/common/src/app/common/data.cljc @@ -93,16 +93,20 @@ (if-let [index (index-of (keys o) before-k)] (let [new-v (if ks (oassoc-in-before (get o k) before-ks ks v) - v)] + v) + current-index (index-of (keys o) k) + new-index (if (and current-index (< current-index index)) + (dec index) + index)] (if (= k before-k) (-> (ordered-map) - (into (take index o)) + (into (take new-index o)) (assoc k new-v) - (into (drop (inc index) o))) + (into (drop (inc new-index) o))) (-> (ordered-map) - (into (take index o)) + (into (take new-index (dissoc o k))) (assoc k new-v) - (into (drop index o))))) + (into (drop new-index (dissoc o k)))))) (oassoc-in o (cons k ks) v))) (defn vec2 diff --git a/common/src/app/common/files/changes.cljc b/common/src/app/common/files/changes.cljc index ec8bb4775..827d4331a 100644 --- a/common/src/app/common/files/changes.cljc +++ b/common/src/app/common/files/changes.cljc @@ -295,6 +295,12 @@ [:name :string] [:token-set ::ctot/token-set]]] + [:move-token-set-before + [:map {:title "MoveTokenSetBefore"} + [:type [:= :move-token-set-before]] + [:set-name :string] + [:before-set-name [:maybe :string]]]] + [:del-token-set [:map {:title "DelTokenSetChange"} [:type [:= :del-token-set]] @@ -865,6 +871,12 @@ (cond-> lib' path-changed? (ctob/update-set-name name (:name token-set))))))) +(defmethod process-change :move-token-set-before + [data {:keys [set-name before-set-name]}] + (update data :tokens-lib #(-> % + (ctob/ensure-tokens-lib) + (ctob/move-set-before set-name before-set-name)))) + (defmethod process-change :del-token-set [data {:keys [name]}] (update data :tokens-lib #(-> % diff --git a/common/src/app/common/files/changes_builder.cljc b/common/src/app/common/files/changes_builder.cljc index 57978d0d2..1c11f58bc 100644 --- a/common/src/app/common/files/changes_builder.cljc +++ b/common/src/app/common/files/changes_builder.cljc @@ -764,6 +764,13 @@ (update :undo-changes conj {:type :add-token-set :token-set prev-token-theme}) (apply-changes-local)))) +(defn move-token-set-before + [changes set-name before-set-name prev-before-set-name] + (-> changes + (update :redo-changes conj {:type :move-token-set-before :set-name set-name :before-set-name before-set-name}) + (update :undo-changes conj {:type :move-token-set-before :set-name set-name :before-set-name prev-before-set-name}) + (apply-changes-local))) + (defn add-token [changes set-name token] (-> changes diff --git a/common/src/app/common/types/tokens_lib.cljc b/common/src/app/common/types/tokens_lib.cljc index 0cf38a80a..3c2a55ee1 100644 --- a/common/src/app/common/types/tokens_lib.cljc +++ b/common/src/app/common/types/tokens_lib.cljc @@ -167,6 +167,17 @@ ;; === Token Set +(def set-separator "/") + +(defn get-token-set-path [path] + (get-path path set-separator)) + +(defn get-token-set-group-str [path] + (get-groups-str path set-separator)) + +(defn split-token-set-path [path] + (split-path path set-separator)) + (defprotocol ITokenSet (add-token [_ token] "add a token at the end of the list") (update-token [_ token-name f] "update a token in the list") @@ -257,11 +268,14 @@ (add-set [_ token-set] "add a set to the library, at the end") (update-set [_ set-name f] "modify a set in the ilbrary") (delete-set [_ set-name] "delete a set in the library") + (move-set-before [_ set-name before-set-name] "move a set with `set-name` before a set with `before-set-name` in the library. +When `before-set-name` is nil, move set to bottom") (set-count [_] "get the total number if sets in the library") (get-set-tree [_] "get a nested tree of all sets in the library") (get-sets [_] "get an ordered sequence of all sets in the library") (get-ordered-set-names [_] "get an ordered sequence of all sets names in the library") (get-set [_ set-name] "get one set looking for name") + (get-neighbor-set-name [_ set-name index-offset] "get neighboring set name offset by `index-offset`") (get-set-group [_ set-group-path] "get the attributes of a set group")) (def schema:token-set-node @@ -462,8 +476,8 @@ ITokenSets (add-set [_ token-set] (dm/assert! "expected valid token set" (check-token-set! token-set)) - (let [path (get-path token-set "/") - groups-str (get-groups-str token-set "/")] + (let [path (get-token-set-path token-set) + groups-str (get-token-set-group-str token-set)] (TokensLib. (d/oassoc-in sets path token-set) (cond-> set-groups (not (str/empty? groups-str)) @@ -472,7 +486,7 @@ active-themes))) (update-set [this set-name f] - (let [path (split-path set-name "/") + (let [path (split-token-set-path set-name) set (get-in sets path)] (if set (let [set' (-> (make-token-set (f set)) @@ -490,12 +504,30 @@ this))) (delete-set [_ set-name] - (let [path (split-path set-name "/")] + (let [path (split-token-set-path set-name)] (TokensLib. (d/dissoc-in sets path) set-groups ;; TODO remove set-group if needed themes active-themes))) + ;; TODO Handle groups and nesting + (move-set-before [this set-name before-set-name] + (let [source-path (split-token-set-path set-name) + token-set (-> (get-set this set-name) + (assoc :modified-at (dt/now))) + target-path (split-token-set-path before-set-name)] + (if before-set-name + (TokensLib. (d/oassoc-in-before sets target-path source-path token-set) + set-groups ;; TODO remove set-group if needed + themes + active-themes) + (TokensLib. (-> sets + (d/dissoc-in source-path) + (d/oassoc-in source-path token-set)) + set-groups ;; TODO remove set-group if needed + themes + active-themes)))) + (get-set-tree [_] sets) @@ -513,6 +545,13 @@ (let [path (split-path set-name "/")] (get-in sets path))) + (get-neighbor-set-name [this set-name index-offset] + (let [sets (get-ordered-set-names this) + index (d/index-of sets set-name) + neighbor-set-name (when index + (nth sets (+ index-offset index) nil))] + neighbor-set-name)) + (get-set-group [_ set-group-path] (get set-groups set-group-path)) diff --git a/common/test/common_tests/types/tokens_lib_test.cljc b/common/test/common_tests/types/tokens_lib_test.cljc index cfb6920ef..64c164fe5 100644 --- a/common/test/common_tests/types/tokens_lib_test.cljc +++ b/common/test/common_tests/types/tokens_lib_test.cljc @@ -79,8 +79,32 @@ (let [args {:name 777 :description 999}] (t/is (thrown-with-msg? Exception #"expected valid token set" - (apply ctob/make-token-set args)))))) + (apply ctob/make-token-set args))))) + (t/deftest move-token-set + (let [tokens-lib (-> (ctob/make-tokens-lib) + (ctob/add-set (ctob/make-token-set :name "A")) + (ctob/add-set (ctob/make-token-set :name "B")) + (ctob/add-set (ctob/make-token-set :name "Move"))) + original-order (into [] (ctob/get-ordered-set-names tokens-lib)) + move (fn [set-name before-set-name] + (->> (ctob/move-set-before tokens-lib set-name before-set-name) + (ctob/get-ordered-set-names) + (into [])))] + ;; TODO Nested moving doesn't work as expected + (t/testing "regular moving" + (t/is (= ["A" "Move" "B"] (move "Move" "B"))) + (t/is (= ["B" "A" "Move"] (move "A" "Move")))) + + (t/testing "move to bottom" + (t/is (= ["B" "Move" "A"] (move "A" nil)))) + + (t/testing "no move expected" + (t/is (= original-order (move "Move" "Move")))) + + (t/testing "ignore invalid moves" + (t/is (= original-order (move "A" "foo/bar/baz"))) + (t/is (= original-order (move "Missing" "Move"))))))) (t/testing "token-theme" (t/deftest make-token-theme diff --git a/frontend/src/app/main/data/tokens.cljs b/frontend/src/app/main/data/tokens.cljs index c725f68a0..a6f1c8b41 100644 --- a/frontend/src/app/main/data/tokens.cljs +++ b/frontend/src/app/main/data/tokens.cljs @@ -204,6 +204,21 @@ (dch/commit-changes changes) (wtu/update-workspace-tokens)))))) +(defn move-token-set [source-set-name dest-set-name position] + (ptk/reify ::move-token-set + ptk/WatchEvent + (watch [it state _] + (let [tokens-lib (get-tokens-lib state) + prev-before-set-name (ctob/get-neighbor-set-name tokens-lib source-set-name 1) + [source-set-name' dest-set-name'] (if (= :top position) + [source-set-name dest-set-name] + [source-set-name (ctob/get-neighbor-set-name tokens-lib dest-set-name 1)]) + changes (-> (pcb/empty-changes it) + (pcb/move-token-set-before source-set-name' dest-set-name' prev-before-set-name))] + (rx/of + (dch/commit-changes changes) + (wtu/update-workspace-tokens)))))) + (defn update-create-token [{:keys [token prev-token-name]}] (ptk/reify ::update-create-token diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.cljs b/frontend/src/app/main/ui/workspace/tokens/sets.cljs index 75854e941..570faa8c0 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sets.cljs @@ -11,6 +11,7 @@ [app.main.data.tokens :as wdt] [app.main.refs :as refs] [app.main.store :as st] + [app.main.ui.hooks :as h] [app.main.ui.icons :as i] [app.main.ui.workspace.tokens.sets-context :as sets-context] [app.util.dom :as dom] @@ -79,26 +80,56 @@ set? true #_(= type :set) group? false #_(= type :group) editing-node? (editing? name) - on-select (mf/use-callback - (mf/deps editing-node?) - (fn [event] - (dom/stop-propagation event) - (when-not editing-node? - (on-select name)))) - on-context-menu (mf/use-callback - (mf/deps editing-node? name) - (fn [event] - (dom/prevent-default event) - (dom/stop-propagation event) - (when-not editing-node? - (st/emit! - (wdt/show-token-set-context-menu - {:position (dom/get-client-position event) - :token-set-name name})))))] - [:div {:class (stl/css :set-item-container) - :on-click on-select + + on-click + (mf/use-callback + (mf/deps editing-node?) + (fn [event] + (dom/stop-propagation event) + (when-not editing-node? + (on-select name)))) + + on-context-menu + (mf/use-callback + (mf/deps editing-node? name) + (fn [event] + (dom/prevent-default event) + (dom/stop-propagation event) + (when-not editing-node? + (st/emit! + (wdt/show-token-set-context-menu + {:position (dom/get-client-position event) + :token-set-name name}))))) + + on-drag + (mf/use-fn + (mf/deps name) + (fn [_] + (when-not selected? + (on-select name)))) + + on-drop + (mf/use-fn + (mf/deps name) + (fn [position data] + (st/emit! (wdt/move-token-set (:name data) name position)))) + + [dprops dref] + (h/use-sortable + :data-type "penpot/token-set" + :on-drag on-drag + :on-drop on-drop + :data {:name name} + :draggable? true)] + [:div {:ref dref + :class (stl/css-case :set-item-container true + :dnd-over (= (:over dprops) :center) + :dnd-over-top (= (:over dprops) :top) + :dnd-over-bot (= (:over dprops) :bot)) + :on-click on-click :on-double-click #(on-edit name) - :on-context-menu on-context-menu} + :on-context-menu on-context-menu + :data-name name} [:div {:class (stl/css-case :set-item-group group? :set-item-set set? :selected-set selected?)} @@ -191,7 +222,7 @@ (let [token-sets (mf/deref refs/workspace-ordered-token-sets) selected-token-set-id (mf/deref refs/workspace-selected-token-set-id) token-set-selected? (mf/use-callback - (mf/deps selected-token-set-id) + (mf/deps token-sets selected-token-set-id) (fn [set-name] (= set-name selected-token-set-id))) active-token-set-ids (mf/deref refs/workspace-active-set-names) diff --git a/frontend/src/app/main/ui/workspace/tokens/sets.scss b/frontend/src/app/main/ui/workspace/tokens/sets.scss index c65fdd2c3..7e4547b15 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sets.scss +++ b/frontend/src/app/main/ui/workspace/tokens/sets.scss @@ -17,6 +17,17 @@ cursor: pointer; color: var(--layer-row-foreground-color); padding-left: $s-20; + border: $s-2 solid transparent; + + &.dnd-over-bot { + border-bottom: $s-2 solid var(--layer-row-foreground-color-hover); + } + &.dnd-over-top { + border-top: $s-2 solid var(--layer-row-foreground-color-hover); + } + &.dnd-over { + border: $s-2 solid var(--layer-row-foreground-color-hover); + } } .set-item-set, diff --git a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs index aae169062..bb6150ae7 100644 --- a/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs +++ b/frontend/src/app/main/ui/workspace/tokens/sidebar.cljs @@ -8,12 +8,14 @@ (:require-macros [app.main.style :as stl]) (:require [app.common.data :as d] + [app.common.types.tokens-lib :as ctob] [app.main.data.modal :as modal] [app.main.data.tokens :as dt] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.color-bullet :refer [color-bullet]] [app.main.ui.components.title-bar :refer [title-bar]] + [app.main.ui.hooks :as h] [app.main.ui.hooks.resize :refer [use-resize-hook]] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.assets.common :as cmm] @@ -32,8 +34,7 @@ [cuerdas.core :as str] [okulary.core :as l] [rumext.v2 :as mf] - [shadow.resource] - [app.common.types.tokens-lib :as ctob])) + [shadow.resource])) (def lens:token-type-open-status (l/derived (l/in [:workspace-tokens :open-status]) st/state)) @@ -205,7 +206,8 @@ :on-collapsed #(swap! open? not)} [:& add-set-button {:on-open on-open}]]] (when @open? - [:& sets-list])]])) + [:& h/sortable-container {} + [:& sets-list]])]])) (mf/defc tokens-explorer [_props]