0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-05 22:09:06 -05:00

♻️ Fix swap component

This commit is contained in:
Eva 2024-02-01 13:16:04 +01:00 committed by Andrey Antukh
parent 532a656daf
commit ced1f60940
7 changed files with 373 additions and 270 deletions

View file

@ -543,6 +543,11 @@
[path-vec]
(str/join " / " path-vec))
(defn join-path-with-dot
"Regenerate a path as a string, from a vector."
[path-vec]
(str/join "\u00A0\u2022\u00A0" path-vec))
(defn clean-path
"Remove empty items from the path."
[path]
@ -608,6 +613,14 @@
""
(join-path (butlast split)))))
(defn butlast-path-with-dots
"Remove the last item of the path."
[path]
(let [split (split-path path)]
(if (= 1 (count split))
""
(join-path-with-dot (butlast split)))))
(defn last-path
"Returns the last item of the path."
[path]

View file

@ -563,7 +563,7 @@
padding: $s-12;
border-radius: $br-8;
z-index: $z-index-10;
color: var(--color-foreground-primary);
color: var(--modal-title-foreground-color);
background-color: var(--modal-background-color);
}
@ -575,7 +575,7 @@
height: 100%;
width: 100%;
z-index: $z-index-modal;
background-color: var(--color-background-subtle);
background-color: var(--overlay-color);
}
.modal-container-base {

View file

@ -222,6 +222,7 @@
--assets-item-background-color: var(--color-background-tertiary);
--assets-item-background-color-hover: var(--color-background-quaternary);
--assets-item-name-background-color: var(--db-secondary-80); // TODO: penpot file has a non-existing token
--assets-item-name-foreground-color-rest: var(--color-foreground-secondary);
--assets-item-name-foreground-color: var(--color-foreground-primary);
--assets-item-name-foreground-color-hover: var(--color-foreground-primary);
--assets-item-name-foreground-color-disabled: var(--color-foreground-disabled);

View file

@ -22,6 +22,8 @@
placeholder (unchecked-get props "placeholder")
icon (unchecked-get props "icon")
autofocus (unchecked-get props "auto-focus")
id (unchecked-get props "id")
handle-change
(mf/use-fn
@ -51,7 +53,8 @@
children
[:div {:class (stl/css :search-input-wrapper)}
icon
[:input {:on-change handle-change
[:input {:id id
:on-change handle-change
:value value
:auto-focus autofocus
:placeholder placeholder

View file

@ -176,20 +176,6 @@
align-items: center;
}
.listing-option-btn {
@include flexCenter;
cursor: pointer;
background-color: var(--button-radio-background-color-rest);
&.first {
margin-left: auto;
}
svg {
@extend .button-icon;
}
}
.add-component {
@extend .button-tertiary;
height: $s-32;

View file

@ -7,6 +7,7 @@
(ns app.main.ui.workspace.sidebar.options.menus.component
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.types.component :as ctk]
[app.common.types.file :as ctf]
@ -28,6 +29,7 @@
[app.main.ui.workspace.sidebar.assets.common :as cmm]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.timers :as tm]
[cuerdas.core :as str]
[rumext.v2 :as mf]))
@ -197,18 +199,21 @@
(mf/defc component-group-item
[{:keys [item on-enter-group] :as props}]
(let [group-name (:name item)
path (cfh/butlast-path group-name)
path (cfh/butlast-path-with-dots group-name)
on-group-click #(on-enter-group group-name)]
[:div {:class (stl/css :component-group)
:key (uuid/next) :on-click on-group-click
:title group-name}
[:div
[:div {:class (stl/css :path-wrapper)}
(when-not (str/blank? path)
[:span {:class (stl/css :component-group-path)}
(str "\u00A0/\u00A0" path)])
(str "\u00A0\u2022\u00A0" path)])
[:span {:class (stl/css :component-group-name)}
(cfh/last-path group-name)]]
[:span i/arrow-refactor]]))
[:span {:class (stl/css :arrow-icon)}
i/arrow-refactor]]))
(mf/defc component-swap
[{:keys [shapes] :as props}]
@ -228,7 +233,9 @@
file-id (if every-same-file?
(:component-file shape)
current-file-id)
orig-components (map #(ctf/get-component libraries (:component-file %) (:component-id %)) shapes)
paths (->> orig-components
(map :path)
(map cfh/split-path))
@ -245,6 +252,7 @@
(cfh/join-path (if (not every-same-file?)
""
(find-common-path [] 0))))
filters* (mf/use-state
{:term ""
:file-id file-id
@ -252,7 +260,9 @@
:listing-thumbs? false})
filters (deref filters*)
is-search? (not (str/blank? (:term filters)))
current-library-id (if (contains? libraries (:file-id filters))
(:file-id filters)
current-file-id)
@ -264,7 +274,7 @@
components (->> (get-in libraries [current-library-id :data :components])
vals
(remove #(true? (:deleted %)))
(map #(assoc % :full-name (cfh/merge-path-item (:path %) (:name %)))))
(map #(assoc % :full-name (cfh/merge-path-item-with-dot (:path %) (:name %)))))
get-subgroups (fn [path]
(let [split-path (cfh/split-path path)]
@ -335,89 +345,99 @@
toggle-list-style
(mf/use-fn
(fn [style]
(swap! filters* assoc :listing-thumbs? (= style "grid"))))]
(swap! filters* assoc :listing-thumbs? (= style "grid"))))
filters-but-last (cfh/butlast-path (:path filters))
last-filters (cfh/last-path (:path filters))
filter-path-with-dots (->> filters-but-last (cfh/split-path) (cfh/join-path-with-dot))]
[:div {:class (stl/css :component-swap)}
[:div {:class (stl/css :element-set-title)}
[:span (tr "workspace.options.component.swap")]]
[:div {:class (stl/css :component-swap-content)}
[:div {:class (stl/css :search-field)}
[:& search-bar {:on-change on-search-term-change
:clear-action on-search-clear-click
:value (:term filters)
:placeholder (str (tr "labels.search") " " (get-in libraries [current-library-id :name]))
:icon (mf/html [:span {:class (stl/css :search-icon)} i/search-refactor])}]]
[:div {:class (stl/css :fields-wrapper)}
[:div {:class (stl/css :search-field)}
[:& search-bar {:on-change on-search-term-change
:clear-action on-search-clear-click
:class (stl/css :search-wrapper)
:id "swap-component-search-filter"
:value (:term filters)
:placeholder (str (tr "labels.search") " " (get-in libraries [current-library-id :name]))
:icon (mf/html [:span {:class (stl/css :search-icon)} i/search-refactor])}]]
[:div {:class (stl/css :select-field)}
[:& select {:class (stl/css :select-library)
:default-value current-library-id
:options libraries-options
:on-change on-library-change}]]
[:div {:class (stl/css :library-name)} current-library-name]
[:div {:class (stl/css :swap-wrapper)}
[:div {:class (stl/css :library-name-wrapper)}
[:div {:class (stl/css :library-name)} current-library-name]
[:div {:class (stl/css :listing-options-wrapper)}
[:& radio-buttons {:class (stl/css :listing-options)
:selected (if (:listing-thumbs? filters) "grid" "list")
:on-change toggle-list-style
:name "swap-listing-style"}
[:& radio-button {:icon i/view-as-list-refactor
:icon-class (stl/css :radio-button)
:value "list"
:id "swap-opt-list"}]
[:& radio-button {:icon i/flex-grid-refactor
:icon-class (stl/css :radio-button)
:value "grid"
:id "swap-opt-grid"}]]]
[:div {:class (stl/css :listing-options-wrapper)}
[:& radio-buttons {:class (stl/css :listing-options)
:selected (if (:listing-thumbs? filters) "grid" "list")
:on-change toggle-list-style
:name "swap-listing-style"}
[:& radio-button {:icon i/view-as-list-refactor
:value "list"
:id "swap-opt-list"}]
[:& radio-button {:icon i/flex-grid-refactor
:value "grid"
:id "swap-opt-grid"}]]]]
(when-not (or is-search? (str/empty? (:path filters)))
[:button {:class (stl/css :component-path)
:on-click on-go-back
:title filter-path-with-dots}
[:span {:class (stl/css :back-arrow)} i/arrow-refactor]
(when-not (= "" filter-path-with-dots)
[:span {:class (stl/css :path-name)}
(dm/str "\u00A0\u2022\u00A0" filter-path-with-dots)])
[:span {:class (stl/css :path-name-last)} last-filters]])
(if (or is-search? (str/empty? (:path filters)))
[:div {:class (stl/css :component-path-empty)}]
[:button {:class (stl/css :component-path)
:on-click on-go-back
:title (:path filters)}
[:span i/arrow-refactor]
[:span (:path filters)]])
(when (empty? items)
[:div {:class (stl/css :component-list-empty)}
(tr "workspace.options.component.swap.empty")]) ;;TODO review this empty space
(when (empty? items)
[:div {:class (stl/css :component-list-empty)}
(tr "workspace.options.component.swap.empty")])
(when (:listing-thumbs? filters)
[:div {:class (stl/css :component-list)}
(for [item groups]
[:& component-group-item {:item item :on-enter-group on-enter-group}])])
(when (:listing-thumbs? filters)
[:div {:class (stl/css :component-list)}
(for [item groups]
[:& component-group-item {:item item :on-enter-group on-enter-group}])])
[:div {:class (stl/css-case :component-grid (:listing-thumbs? filters)
:component-list (not (:listing-thumbs? filters)))}
(for [item items]
(if (:id item)
(let [data (get-in libraries [current-library-id :data])
container (ctf/get-component-page data item)
root-shape (ctf/get-component-root data item)
loop? (or (contains? parent-components (:main-instance-id item))
(contains? parent-components (:id item)))]
[:& component-swap-item {:key (:id item)
:item item
:loop loop?
:shapes shapes
:file-id current-library-id
:root-shape root-shape
:container container
:component-id current-comp-id
:is-search is-search?
:listing-thumbs (:listing-thumbs? filters)}])
[:& component-group-item {:item item :on-enter-group on-enter-group}]))]]]))
[:div {:class (stl/css-case :component-grid (:listing-thumbs? filters)
:component-list (not (:listing-thumbs? filters)))}
(for [item items]
(if (:id item)
(let [data (get-in libraries [current-library-id :data])
container (ctf/get-component-page data item)
root-shape (ctf/get-component-root data item)
loop? (or (contains? parent-components (:main-instance-id item))
(contains? parent-components (:id item)))]
[:& component-swap-item {:key (:id item)
:item item
:loop loop?
:shapes shapes
:file-id current-library-id
:root-shape root-shape
:container container
:component-id current-comp-id
:is-search is-search?
:listing-thumbs (:listing-thumbs? filters)}])
[:& component-group-item {:item item
:key (:id item)
:on-enter-group on-enter-group}]))]]]]))
(mf/defc component-ctx-menu
[{:keys [menu-entries on-close show] :as props}]
[{:keys [menu-entries on-close show main-instance] :as props}]
(let [do-action
(fn [action event]
(dom/stop-propagation event)
(action)
(on-close))]
[:& dropdown {:show show :on-close on-close}
[:ul {:class (stl/css :custom-select-dropdown)}
[:ul {:class (stl/css-case :custom-select-dropdown true
:not-main (not main-instance))}
(for [entry menu-entries :when (not (nil? entry))]
[:li {:key (uuid/next)
:class (stl/css :dropdown-element)
@ -471,10 +491,14 @@
open-component-panel
(mf/use-fn
(mf/deps can-swap? shapes)
#(when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap))))
(fn []
(let [search-id "swap-component-search-filter"]
(when can-swap? (st/emit! (dwsp/open-specialized-panel :component-swap)))
(tm/schedule-on-idle #(dom/focus! (dom/get-element search-id))))))
menu-entries (cmm/generate-components-menu-entries shapes components-v2)
show-menu? (seq menu-entries)]
show-menu? (seq menu-entries)
path (->> component (:path) (cfh/split-path) (cfh/join-path-with-dot))]
(when (seq shapes)
[:div {:class (stl/css :element-set)}
@ -482,8 +506,9 @@
(if swap-opened?
[:button {:class (stl/css :title-back)
:on-click on-component-back}
[:span i/arrow-refactor]
[:span {:class (stl/css :icon-back)} i/arrow-refactor]
[:span (tr "workspace.options.component")]]
[:& title-bar {:collapsable true
:collapsed (not open?)
:on-collapsed toggle-content
@ -496,31 +521,40 @@
(when open?
[:div {:class (stl/css :element-content)}
[:div {:class (stl/css :component-wrapper)}
[:div {:class (stl/css-case :component-name-wrapper true
:with-main (and can-swap? (not multi))
:swappeable (and can-swap? (not swap-opened?)))
:on-click open-component-panel}
[:div {:class (stl/css-case :component-wrapper true
:with-actions show-menu?)}
[:button {:class (stl/css-case :component-name-wrapper true
:with-main (and can-swap? (not multi))
:swappeable (and can-swap? (not swap-opened?)))
:on-click open-component-panel}
[:span {:class (stl/css :component-icon)}
(if main-instance?
i/component-refactor
i/copy-refactor)]
[:div {:class (stl/css :component-name)} (if multi
(tr "settings.multiple")
(cfh/last-path shape-name))]
(when show-menu?
[:div {:class (stl/css :component-actions)}
[:button {:class (stl/css :menu-btn)
:on-click on-menu-click}
i/menu-refactor]
[:div {:class (stl/css :name-wrapper)}
[:div {:class (stl/css :component-name)}
(if multi
(tr "settings.multiple")
(cfh/last-path shape-name))]
(when (and can-swap? (not multi))
[:div {:class (stl/css :component-parent-name)}
(cfh/merge-path-item-with-dot path (:name component))])]]
(when show-menu?
[:div {:class (stl/css :component-actions)}
[:button {:class (stl/css-case :menu-btn true
:selected menu-open?)
:on-click on-menu-click}
i/menu-refactor]
[:& component-ctx-menu {:show menu-open?
:on-close on-menu-close
:menu-entries menu-entries
:main-instance main-instance?}]])]
[:& component-ctx-menu {:show menu-open?
:on-close on-menu-close
:menu-entries menu-entries}]])
(when (and can-swap? (not multi))
[:div {:class (stl/css :component-parent-name)}
(cfh/merge-path-item (:path component) (:name component))])]]
(when swap-opened?
[:& component-swap {:shapes copies}])

View file

@ -7,33 +7,132 @@
@import "refactor/common-refactor.scss";
.element-set {
margin: 0;
padding-top: $s-8;
}
.element-content {
@include flexColumn;
margin-bottom: $s-8;
}
.title-back {
@include tabTitleTipography;
display: flex;
align-items: center;
gap: $s-4;
width: 100%;
height: $s-32;
padding: 0;
border: 0;
border-radius: $br-8;
background-color: var(--title-background-color);
color: var(--title-foreground-color);
cursor: pointer;
}
.icon-back {
@include flexCenter;
width: $s-12;
height: 100%;
svg {
height: $s-12;
width: $s-12;
stroke: var(--icon-foreground);
transform: rotate(180deg);
}
}
.component-wrapper {
display: flex;
margin: 0 $s-4 0 $s-8;
width: 100%;
min-height: $s-32;
border-radius: $br-8;
&.with-actions {
display: grid;
grid-template-columns: 1fr $s-28;
gap: $s-2;
}
}
.component-name-wrapper {
@extend .asset-element;
@include flexRow;
flex-grow: 1;
height: 100%;
width: 100%;
flex-wrap: wrap;
padding: 0 0 0 $s-12;
margin-top: $s-8;
&.with-main {
padding-bottom: $s-12;
@include buttonStyle;
cursor: default;
display: grid;
grid-template-columns: $s-12 1fr;
gap: $s-4;
padding: 0 $s-8;
border-radius: $br-8 0 0 $br-8;
background-color: var(--assets-item-background-color);
color: var(--assets-item-name-foreground-color-hover);
&:hover {
background-color: var(--assets-item-background-color-hover);
color: var(--assets-item-name-foreground-color-hover);
}
}
.component-icon {
@include flexCenter;
height: $s-32;
width: $s-12;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
}
.name-wrapper {
@include flexColumn;
min-height: $s-32;
padding: $s-8 0 $s-8 $s-2;
border-radius: $br-8 0 0 $br-8;
}
.component-name {
@include titleTipography;
@include textEllipsis;
direction: rtl;
text-align: left;
min-height: $s-16;
}
.component-parent-name {
@include titleTipography;
@include textEllipsis;
direction: rtl;
text-align: left;
min-height: $s-16;
max-width: $s-184;
color: var(--title-foreground-color);
}
.component-actions {
position: relative;
}
.menu-btn {
@extend .button-tertiary;
height: 100%;
width: $s-28;
border-radius: 0 $br-8 $br-8 0;
background-color: var(--assets-item-background-color);
color: var(--assets-item-name-foreground-color-hover);
svg {
@extend .button-icon;
min-height: $s-16;
min-width: $s-16;
}
&:hover {
background-color: var(--assets-item-background-color-hover);
color: var(--assets-item-name-foreground-color-hover);
&.selected {
@extend .button-icon-selected;
}
}
}
.menu-btn.selected {
@extend .button-icon-selected;
}
.copy-text {
@include titleTipography;
height: 100%;
@ -43,52 +142,10 @@
margin-right: $s-8;
}
.component-icon {
@include flexCenter;
height: $s-24;
width: $s-24;
svg {
@extend .button-icon;
stroke: var(--icon-foreground);
}
}
.component-name {
@include titleTipography;
@include textEllipsis;
direction: rtl;
text-align: left;
width: 70%;
flex-grow: 2;
margin-left: $s-8;
}
.component-parent-name {
@include titleTipography;
@include textEllipsis;
text-align: left;
max-width: 95%;
padding-left: $s-36;
color: var(--title-foreground-color);
}
.swappeable {
cursor: pointer;
}
.component-actions {
position: relative;
}
.menu-btn {
@extend .button-tertiary;
height: $s-32;
width: $s-28;
svg {
@extend .button-icon;
}
}
.custom-select-dropdown {
@extend .dropdown-wrapper;
right: 0;
@ -96,44 +153,14 @@
width: $s-252;
}
.not-main {
top: $s-56;
}
.dropdown-element {
@extend .dropdown-element-base;
}
.title-back {
@include tabTitleTipography;
cursor: pointer;
width: 100%;
background-color: var(--title-background-color);
color: var(--title-foreground-color);
text-align: left;
border: 0;
margin-bottom: $s-16;
svg {
height: $s-8;
width: $s-8;
stroke: var(--icon-foreground);
margin-right: $s-16;
transform: rotate(180deg);
}
}
.search-field {
display: flex;
align-items: center;
height: $s-32;
margin: $s-16 $s-4 $s-4 $s-12;
border-radius: $br-8;
font-family: "worksans", sans-serif;
background-color: var(--input-background-color);
}
.search-box {
align-items: center;
display: flex;
width: 100%;
}
.icon-wrapper {
display: flex;
svg {
@ -175,52 +202,52 @@
.search-icon {
@include flexCenter;
width: $s-28;
width: $s-12;
margin-left: $s-8;
svg {
@extend .button-icon-small;
stroke: var(--icon-foreground);
}
}
.select-field {
margin: $s-8 $s-4 0 $s-12;
}
.select-library {
padding-left: $s-20;
}
.listing-options-wrapper {
width: 100%;
}
.listing-options {
margin-left: auto;
margin-right: $s-4;
}
.component-path {
@include titleTipography;
@include textEllipsis;
text-align: left;
cursor: pointer;
display: flex;
align-items: center;
gap: $s-4;
width: 100%;
height: $s-32;
padding: 0;
border: 0;
background-color: var(--title-background-color);
color: var(--title-foreground-color);
border: 0;
margin: $s-16 0 $s-12 0;
padding: 0 $s-16 0 $s-24;
cursor: pointer;
}
.back-arrow {
@include flexCenter;
height: $s-32;
svg {
height: $s-8;
width: $s-8;
height: $s-12;
width: $s-12;
stroke: var(--icon-foreground);
margin-right: $s-16;
transform: rotate(180deg);
}
}
.component-path-empty {
height: $s-16;
.path-name {
@include titleTipography;
@include textEllipsis;
direction: rtl;
height: $s-32;
padding: $s-8 0 $s-8 $s-2;
}
.path-name-last {
@include titleTipography;
@include textEllipsis;
height: $s-32;
padding: $s-8 0 $s-8 $s-2;
color: white;
}
.component-list-empty {
@ -229,10 +256,6 @@
color: $df-secondary;
}
.component-list {
margin: 0 $s-4 0 $s-8;
}
.component-item {
display: flex;
align-items: center;
@ -280,40 +303,6 @@
}
}
.component-group {
@include titleTipography;
text-align: left;
display: flex;
align-items: center;
margin: 0 $s-16 $s-8 $s-8;
justify-content: space-between;
cursor: pointer;
height: $s-24;
svg {
height: $s-8;
width: $s-8;
}
div {
display: flex;
width: 90%;
}
span {
@include textEllipsis;
}
.component-group-path {
direction: rtl;
}
.component-group-name {
color: var(--assets-item-name-foreground-color);
}
&:hover {
color: var(--assets-item-name-foreground-color-hover);
.component-group-name {
color: var(--assets-item-name-foreground-color-hover);
}
}
}
.component-grid {
display: grid;
grid-template-columns: repeat(2, $s-124);
@ -393,30 +382,107 @@
}
}
.element-set-title {
@include tabTitleTipography;
display: flex;
align-items: center;
height: $s-32;
padding-left: $s-2;
color: var(--title-foreground-color);
}
// Component swap
.component-swap {
padding-top: $s-12;
}
.component-swap-content {
@include flexColumn;
gap: $s-16;
}
.fields-wrapper {
@include flexColumn;
gap: $s-4;
}
.search-field {
display: flex;
align-items: center;
height: $s-32;
border-radius: $br-8;
font-family: "worksans", sans-serif;
background-color: var(--input-background-color);
}
.library-name-wrapper {
display: grid;
grid-template-columns: 1fr auto;
}
.library-name {
@include titleTipography;
@include textEllipsis;
margin: $s-20 $s-4 0 $s-12;
color: var(--title-foreground-color);
padding: $s-8 0 $s-8 $s-2;
}
.element-set-title {
.swap-wrapper {
@include flexColumn;
gap: $s-4;
}
.listing-options-wrapper {
width: 100%;
}
.listing-options {
display: flex;
align-items: center;
}
.component-group {
@include titleTipography;
text-transform: uppercase;
margin: $s-16 $s-4 0 $s-12;
color: var(--title-foreground-color);
display: grid;
grid-template-columns: 1fr $s-12;
height: $s-32;
cursor: pointer;
.component-group-name {
@include textEllipsis;
color: var(--assets-item-name-foreground-color);
}
&:hover {
color: var(--assets-item-name-foreground-color-hover);
.component-group-name {
color: var(--assets-item-name-foreground-color-hover);
}
}
}
.radio-button {
.arrow-icon {
@include flexCenter;
height: $s-32;
svg {
stroke: var(--icon-foreground);
fill: var(--icon-foreground);
height: $s-12;
width: $s-12;
cursor: pointer;
stroke: var(--icon-foreground);
}
}
.path-wrapper {
display: flex;
max-width: $s-232;
padding: $s-8 0 $s-8 $s-2;
}
.component-group-path {
@include textEllipsis;
direction: rtl;
color: var(--assets-item-name-foreground-color-rest);
}
// Component annotation
.component-annotation {