Add view mode to dashboard

This commit is contained in:
Pablo Alba 2024-09-26 16:28:31 +02:00
parent c841ed6419
commit cf150891df
36 changed files with 699 additions and 240 deletions

View file

@ -170,3 +170,26 @@
(->> (rp/cmd! :create-team-access-request params)
(rx/tap on-success)
(rx/catch on-error))))))
(defn change-team-permissions
[team-id role]
(ptk/reify ::change-team-permissions
ptk/UpdateEvent
(update [_ state]
(update-in state [:teams team-id :permissions]
(fn [permissions]
(cond
(= role :viewer)
(assoc permissions :can-edit false :is-admin false :is-owner false)
(= role :editor)
(assoc permissions :can-edit true :is-admin false :is-owner false)
(= role :admin)
(assoc permissions :can-edit true :is-admin true :is-owner false)
(= role :owner)
(assoc permissions :can-edit true :is-admin true :is-owner true)
:else
permissions))))))

View file

@ -12,17 +12,20 @@
[app.common.files.helpers :as cfh]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.types.team :as tt]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
[app.main.data.common :refer [handle-notification]]
[app.main.data.common :as dc]
[app.main.data.events :as ev]
[app.main.data.fonts :as df]
[app.main.data.media :as di]
[app.main.data.notifications :as ntf]
[app.main.data.users :as du]
[app.main.data.websocket :as dws]
[app.main.features :as features]
[app.main.repo :as rp]
[app.main.store :as st]
[app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]]
[app.util.router :as rt]
@ -42,6 +45,7 @@
(declare fetch-projects)
(declare fetch-team-members)
(declare process-message)
(defn initialize
[{:keys [id]}]
@ -77,11 +81,10 @@
(->> stream
(rx/filter (ptk/type? ::dws/message))
(rx/map deref)
(rx/filter (fn [{:keys [subs-id type] :as msg}]
(and (or (= subs-id uuid/zero)
(= subs-id profile-id))
(= :notification type))))
(rx/map handle-notification))
(rx/filter (fn [{:keys [subs-id] :as msg}]
(or (= subs-id uuid/zero)
(= subs-id profile-id))))
(rx/map process-message))
;; Once the teams are fecthed, initialize features related
;; to currently active team
@ -477,10 +480,32 @@
:team-id team-id}))))
(rx/catch on-error))))))
(defn handle-team-permissions-change
[{:keys [role team-id]}]
(dm/assert! (uuid? team-id))
(dm/assert! (contains? tt/valid-roles role))
(let [msg (case role
:viewer
(tr "dashboard.permissions-change.viewer")
:editor
(tr "dashboard.permissions-change.editor")
:admin
(tr "dashboard.permissions-change.admin")
:owner
(tr "dashboard.permissions-change.owner"))]
(st/emit! (ntf/info msg)
(dc/change-team-permissions team-id role))))
(defn update-team-member-role
[{:keys [role member-id] :as params}]
(dm/assert! (uuid? member-id))
(dm/assert! (keyword? role)) ; FIXME: validate proper role?
(dm/assert! (contains? tt/valid-roles role))
(ptk/reify ::update-team-member-role
ptk/WatchEvent
(watch [_ state _]
@ -602,7 +627,7 @@
(sm/check-email! email))
(dm/assert! (uuid? team-id))
(dm/assert! (keyword? role)) ;; FIXME validate role
(dm/assert! (contains? tt/valid-roles role))
(ptk/reify ::update-team-invitation-role
IDeref
@ -1203,3 +1228,14 @@
(let [file (get-in state [:dashboard-files (first files)])]
(rx/of (go-to-workspace file)))
(rx/empty))))))
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;; Notifications
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- process-message
[{:keys [type] :as msg}]
(case type
:notification (dc/handle-notification msg)
:team-permissions-change (handle-team-permissions-change msg)
nil))

View file

@ -65,6 +65,7 @@
content-width (mf/use-state 0)
project-id (:id project)
team-id (:id team)
you-viewer? (not (get-in team [:permissions :can-edit]))
dashboard-local (mf/deref refs/dashboard-local)
file-menu-open? (:menu-open dashboard-local)
@ -84,7 +85,10 @@
clear-selected-fn
(mf/use-fn
#(st/emit! (dd/clear-selected-files)))]
#(st/emit! (dd/clear-selected-files)))
show-templates (and (contains? cf/flags :dashboard-templates-section)
(not you-viewer?))]
(mf/with-effect []
(let [key1 (events/listen js/window "resize" on-resize)]
@ -105,7 +109,7 @@
:profile profile
:default-project-id default-project-id}]
(when (contains? cf/flags :dashboard-templates-section)
(when show-templates
[:& templates-section {:profile profile
:project-id project-id
:team-id team-id
@ -113,7 +117,7 @@
:content-width @content-width}])]
:dashboard-fonts
[:& fonts-page {:team team}]
[:& fonts-page {:team team :you-viewer? you-viewer?}]
:dashboard-font-providers
[:& font-providers-page {:team team}]
@ -121,8 +125,8 @@
:dashboard-files
(when project
[:*
[:& files-section {:team team :project project}]
(when (contains? cf/flags :dashboard-templates-section)
[:& files-section {:team team :project project :you-viewer? you-viewer?}]
(when show-templates
[:& templates-section {:profile profile
:team-id team-id
:project-id project-id
@ -134,7 +138,7 @@
:search-term search-term}]
:dashboard-libraries
[:& libraries-page {:team team}]
[:& libraries-page {:team team :you-viewer? you-viewer?}]
:dashboard-team-members
[:& team-members-page {:team team :profile profile :invite-email invite-email}]

View file

@ -11,6 +11,7 @@
[app.main.data.events :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
[app.main.refs :as refs]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]]
@ -55,7 +56,7 @@
(mf/defc file-menu
{::mf/wrap-props false}
[{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id]}]
[{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id you-viewer?]}]
(assert (seq files) "missing `files` prop")
(assert (boolean? show?) "missing `show?` prop")
(assert (fn? on-edit) "missing `on-edit` prop")
@ -73,7 +74,10 @@
current-team-id (mf/use-ctx ctx/current-team-id)
teams (mf/use-state nil)
current-team (get @teams current-team-id)
default-team (-> (mf/deref refs/teams)
(get current-team-id))
current-team (or (get @teams current-team-id) default-team)
other-teams (remove #(= (:id %) current-team-id) (vals @teams))
current-projects (remove #(= (:id %) (:project-id file))
(:projects current-team))
@ -237,11 +241,13 @@
(:id sub-project))})})}]))
options (if multi?
[{:option-name (tr "dashboard.duplicate-multi" file-count)
:id "file-duplicate-multi"
:option-handler on-duplicate
:data-testid "duplicate-multi"}
(when (or (seq current-projects) (seq other-teams))
[(when-not you-viewer?
{:option-name (tr "dashboard.duplicate-multi" file-count)
:id "file-duplicate-multi"
:option-handler on-duplicate
:data-testid "duplicate-multi"})
(when (and (or (seq current-projects) (seq other-teams))
(not you-viewer?))
{:option-name (tr "dashboard.move-to-multi" file-count)
:id "file-move-multi"
:sub-options sub-options
@ -252,12 +258,14 @@
{:option-name (tr "dashboard.export-standard-multi" file-count)
:id "file-standard-export-multi"
:option-handler on-export-standard-files}
(when (:is-shared file)
(when (and (:is-shared file)
(not you-viewer?))
{:option-name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi"
:option-handler on-del-shared
:data-testid "file-del-shared"})
(when (not is-lib-page?)
(when (and (not is-lib-page?)
(not you-viewer?))
{:option-name :separator}
{:option-name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi"
@ -267,22 +275,28 @@
[{:option-name (tr "dashboard.open-in-new-tab")
:id "file-open-new-tab"
:option-handler on-new-tab}
(when (not is-search-page?)
(when (and (not is-search-page?)
(not you-viewer?))
{:option-name (tr "labels.rename")
:id "file-rename"
:option-handler on-edit
:data-testid "file-rename"})
(when (not is-search-page?)
(when (and (not is-search-page?)
(not you-viewer?))
{:option-name (tr "dashboard.duplicate")
:id "file-duplicate"
:option-handler on-duplicate
:data-testid "file-duplicate"})
(when (and (not is-lib-page?) (not is-search-page?) (or (seq current-projects) (seq other-teams)))
(when (and (not is-lib-page?)
(not is-search-page?)
(or (seq current-projects) (seq other-teams))
(not you-viewer?))
{:option-name (tr "dashboard.move-to")
:id "file-move-to"
:sub-options sub-options
:data-testid "file-move-to"})
(when (not is-search-page?)
(when (and (not is-search-page?)
(not you-viewer?))
(if (:is-shared file)
{:option-name (tr "dashboard.unpublish-shared")
:id "file-del-shared"
@ -301,7 +315,7 @@
:id "file-download-standard"
:option-handler on-export-standard-files
:data-testid "download-standard-file"}
(when (and (not is-lib-page?) (not is-search-page?))
(when (and (not is-lib-page?) (not is-search-page?) (not you-viewer?))
{:option-name :separator}
{:option-name (tr "labels.delete")
:id "file-delete"

View file

@ -15,6 +15,7 @@
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.pin-button :refer [pin-button*]]
[app.main.ui.dashboard.project-menu :refer [project-menu]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
@ -28,7 +29,7 @@
(i/icon-xref :menu (stl/css :menu-icon)))
(mf/defc header
[{:keys [project create-fn] :as props}]
[{:keys [project create-fn you-viewer?] :as props}]
(let [local (mf/use-state
{:menu-open false
:edition false})
@ -71,7 +72,8 @@
[:div#dashboard-drafts-title {:class (stl/css :dashboard-title)}
[:h1 (tr "labels.drafts")]]
(if (:edition @local)
(if (and (:edition @local)
(not you-viewer?))
[:& inline-edition
{:content (:name project)
:on-end (fn [name]
@ -86,23 +88,16 @@
:id (:id project)}
(:name project)]]))
[:& project-menu {:project project
:show? (:menu-open @local)
:left (- (:x (:menu-pos @local)) 180)
:top (:y (:menu-pos @local))
:on-edit on-edit
:on-menu-close on-menu-close
:on-import on-import}]
[:div {:class (stl/css :dashboard-header-actions)}
[:a {:class (stl/css :btn-secondary :btn-small :new-file)
:tab-index "0"
:on-click on-create-click
:data-testid "new-file"
:on-key-down (fn [event]
(when (kbd/enter? event)
(on-create-click event)))}
(tr "dashboard.new-file")]
(when-not you-viewer?
[:a {:class (stl/css :btn-secondary :btn-small :new-file)
:tab-index "0"
:on-click on-create-click
:data-testid "new-file"
:on-key-down (fn [event]
(when (kbd/enter? event)
(on-create-click event)))}
(tr "dashboard.new-file")])
(when-not (:is-default project)
[:> pin-button*
@ -111,19 +106,30 @@
:on-click toggle-pin
:on-key-down (fn [event] (when (kbd/enter? event) (toggle-pin event)))}])
[:div {:class (stl/css :icon)
:tab-index "0"
:on-click on-menu-click
:title (tr "dashboard.options")
:on-key-down (fn [event]
(when (kbd/enter? event)
(on-menu-click event)))}
menu-icon]]]))
(when-not you-viewer?
[:div {:class (stl/css :icon)
:tab-index "0"
:on-click on-menu-click
:title (tr "dashboard.options")
:on-key-down (fn [event]
(when (kbd/enter? event)
(on-menu-click event)))}
menu-icon])
(when-not you-viewer?
[:& project-menu {:project project
:show? (:menu-open @local)
:left (- (:x (:menu-pos @local)) 180)
:top (:y (:menu-pos @local))
:on-edit on-edit
:on-menu-close on-menu-close
:on-import on-import}])]]))
(mf/defc files-section
[{:keys [project team] :as props}]
[{:keys [project team you-viewer?] :as props}]
(let [files-map (mf/deref refs/dashboard-files)
project-id (:id project)
is-draft-proyect (:is-default project)
[rowref limit] (hooks/use-dynamic-grid-item-width)
@ -132,6 +138,9 @@
(filter #(= project-id (:project-id %)))
(sort-by :modified-at)
(reverse)))
file-count (or (count files) 0)
empty-state-viewer (and you-viewer?
(= 0 file-count))
on-file-created
(mf/use-fn
@ -164,12 +173,23 @@
[:*
[:& header {:team team
:project project
:you-viewer? you-viewer?
:create-fn create-file}]
[:section {:class (stl/css :dashboard-container :no-bg)
:ref rowref}
[:& grid {:project project
:files files
:origin :files
:create-fn create-file
:limit limit}]]]))
(if empty-state-viewer
[:> empty-placeholder* {:title (if is-draft-proyect
(tr "dashboard.empty-placeholder-drafts-title")
(tr "dashboard.empty-placeholder-files-title"))
:class (stl/css :placeholder-placement)
:type 1
:subtitle (if is-draft-proyect
(tr "dashboard.empty-placeholder-drafts-subtitle")
(tr "dashboard.empty-placeholder-files-subtitle"))}]
[:& grid {:project project
:files files
:you-viewer? you-viewer?
:origin :files
:create-fn create-file
:limit limit}])]]))

View file

@ -35,3 +35,7 @@
@extend .button-icon;
stroke: var(--icon-foreground);
}
.placeholder-placement {
margin: $s-16 $s-32;
}

View file

@ -16,6 +16,7 @@
[app.main.store :as st]
[app.main.ui.components.context-menu-a11y :refer [context-menu-a11y]]
[app.main.ui.components.file-uploader :refer [file-uploader]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.icons :as i]
[app.main.ui.notifications.context-notification :refer [context-notification]]
[app.util.dom :as dom]
@ -269,7 +270,7 @@
{::mf/props :obj
::mf/private true
::mf/memo true}
[{:keys [font-id variants]}]
[{:keys [font-id variants you-viewer?]}]
(let [font (first variants)
menu-open* (mf/use-state false)
@ -364,11 +365,12 @@
:key (dm/str id)}
[:span {:class (stl/css :label)}
[:& font-variant-display-name {:variant item}]]
[:span
{:class (stl/css :icon :close)
:data-id (dm/str id)
:on-click on-delete-variant}
i/add]])]
(when-not you-viewer?
[:span
{:class (stl/css :icon :close)
:data-id (dm/str id)
:on-click on-delete-variant}
i/add])])]
(if ^boolean edition?
[:div {:class (stl/css :table-field :options)}
@ -382,19 +384,19 @@
:on-click on-cancel}
i/close]]
[:div {:class (stl/css :table-field :options)}
[:span {:class (stl/css :icon)
:on-click on-menu-open}
i/menu]
(when-not you-viewer? [:div {:class (stl/css :table-field :options)}
[:span {:class (stl/css :icon)
:on-click on-menu-open}
i/menu]
[:& installed-font-context-menu
{:on-close on-menu-close
:is-open menu-open?
:on-delete on-delete-font
:on-edit on-edit}]])]))
[:& installed-font-context-menu
{:on-close on-menu-close
:is-open menu-open?
:on-delete on-delete-font
:on-edit on-edit}]]))]))
(mf/defc installed-fonts
[{:keys [fonts] :as props}]
[{:keys [fonts you-viewer?] :as props}]
(let [sterm (mf/use-state "")
matches?
@ -407,23 +409,24 @@
(reset! sterm (str/lower val)))))]
[:div {:class (stl/css :dashboard-installed-fonts)}
[:h3 (tr "labels.installed-fonts")]
[:div {:class (stl/css :installed-fonts-header)}
[:div {:class (stl/css :table-field :family)} (tr "labels.font-family")]
[:div {:class (stl/css :table-field :variants)} (tr "labels.font-variants")]
[:div {:class (stl/css :table-field :search-input)}
[:input {:placeholder (tr "labels.search-font")
:default-value ""
:on-change on-change}]]]
(cond
(seq fonts)
(for [[font-id variants] (->> (vals fonts)
(filter matches?)
(group-by :font-id))]
[:& installed-font {:key (dm/str font-id "-installed")
:font-id font-id
:variants variants}])
[:*
[:h3 (tr "labels.installed-fonts")]
[:div {:class (stl/css :installed-fonts-header)}
[:div {:class (stl/css :table-field :family)} (tr "labels.font-family")]
[:div {:class (stl/css :table-field :variants)} (tr "labels.font-variants")]
[:div {:class (stl/css :table-field :search-input)}
[:input {:placeholder (tr "labels.search-font")
:default-value ""
:on-change on-change}]]]
(for [[font-id variants] (->> (vals fonts)
(filter matches?)
(group-by :font-id))]
[:& installed-font {:key (dm/str font-id "-installed")
:font-id font-id
:you-viewer? you-viewer?
:variants variants}])]
(nil? fonts)
[:div {:class (stl/css :fonts-placeholder)}
@ -431,18 +434,24 @@
[:div {:class (stl/css :label)} (tr "dashboard.loading-fonts")]]
:else
[:div {:class (stl/css :fonts-placeholder)}
[:div {:class (stl/css :icon)} i/text]
[:div {:class (stl/css :label)} (tr "dashboard.fonts.empty-placeholder")]])]))
(if you-viewer?
[:> empty-placeholder* {:title (tr "dashboard.fonts.empty-placeholder-viewer")
:subtitle (tr "dashboard.fonts.empty-placeholder-viewer-sub")
:type 2}]
[:div {:class (stl/css :fonts-placeholder)}
[:div {:class (stl/css :icon)} i/text]
[:div {:class (stl/css :label)} (tr "dashboard.fonts.empty-placeholder")]]))]))
(mf/defc fonts-page
[{:keys [team] :as props}]
[{:keys [team you-viewer?] :as props}]
(let [fonts (mf/deref refs/dashboard-fonts)]
[:*
[:& header {:team team :section :fonts}]
[:section {:class (stl/css :dashboard-container :dashboard-fonts)}
[:& uploaded-fonts {:team team :installed-fonts fonts}]
[:& installed-fonts {:team team :fonts fonts}]]]))
(when-not you-viewer?
[:& uploaded-fonts {:team team :installed-fonts fonts}])
[:& installed-fonts {:team team :fonts fonts :you-viewer? you-viewer?}]]]))
(mf/defc font-providers-page
[{:keys [team] :as props}]

View file

@ -128,6 +128,7 @@
flex-wrap: wrap;
flex-grow: 1;
padding-left: $s-16;
gap: $s-6;
.variant {
display: flex;
@ -135,13 +136,13 @@
align-items: center;
padding: $s-8 $s-12;
cursor: pointer;
gap: $s-4;
.icon {
display: flex;
align-items: center;
justify-content: center;
height: $s-16;
width: $s-16;
margin-left: $s-6;
align-items: center;
svg {
fill: none;
width: $s-12;
@ -163,8 +164,6 @@
.variant {
background-color: var(--color-background-quaternary);
border-radius: $br-8;
margin-right: $s-4;
padding-right: $s-4;
}
}

View file

@ -73,7 +73,7 @@
(mf/defc grid-item-thumbnail
{::mf/wrap-props false}
[{:keys [file-id revn thumbnail-id background-color]}]
[{:keys [file-id revn thumbnail-id background-color you-viewer?]}]
(let [container (mf/use-ref)
visible? (h/use-visible container :once? true)]
@ -94,10 +94,12 @@
(when visible?
(if thumbnail-id
[:img {:class (stl/css :grid-item-thumbnail-image)
:draggable (dm/str (not you-viewer?))
:src (cf/resolve-media thumbnail-id)
:loading "lazy"
:decoding "async"}]
[:> loader* {:class (stl/css :grid-loader)
:draggable (dm/str (not you-viewer?))
:overlay true
:title (tr "labels.loading")}]))]))
@ -231,7 +233,7 @@
(mf/defc grid-item
{:wrap [mf/memo]}
[{:keys [file origin library-view?] :as props}]
[{:keys [file origin library-view? you-viewer?] :as props}]
(let [file-id (:id file)
;; FIXME: this breaks react hooks rule, hooks should never to
@ -274,33 +276,34 @@
on-drag-start
(mf/use-fn
(mf/deps selected-files)
(mf/deps selected-files you-viewer?)
(fn [event]
(st/emit! (dd/hide-file-menu))
(let [offset (dom/get-offset-position (.-nativeEvent event))
(when-not you-viewer?
(let [offset (dom/get-offset-position (.-nativeEvent event))
select-current? (not (contains? selected-files (:id file)))
select-current? (not (contains? selected-files (:id file)))
item-el (mf/ref-val node-ref)
counter-el (create-counter-element
item-el
(if select-current?
1
(count selected-files)))]
(when select-current?
(st/emit! (dd/clear-selected-files))
(st/emit! (dd/toggle-file-select file)))
item-el (mf/ref-val node-ref)
counter-el (create-counter-element
item-el
(if select-current?
1
(count selected-files)))]
(when select-current?
(st/emit! (dd/clear-selected-files))
(st/emit! (dd/toggle-file-select file)))
(dnd/set-data! event "penpot/files" "dummy")
(dnd/set-allowed-effect! event "move")
(dnd/set-data! event "penpot/files" "dummy")
(dnd/set-allowed-effect! event "move")
;; set-drag-image requires that the element is rendered and
;; visible to the user at the moment of creating the ghost
;; image (to make a snapshot), but you may remove it right
;; afterwards, in the next render cycle.
(dom/append-child! item-el counter-el)
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
(ts/raf #(.removeChild ^js item-el counter-el)))))
(dom/append-child! item-el counter-el)
(dnd/set-drag-image! event item-el (:x offset) (:y offset))
(ts/raf #(.removeChild ^js item-el counter-el))))))
on-menu-click
(mf/use-fn
@ -351,13 +354,12 @@
(on-select event)) ;; TODO Fix this
)))]
[:li
{:class (stl/css-case :grid-item true :project-th true :library library-view?)}
[:li {:class (stl/css-case :grid-item true :project-th true :library library-view?)}
[:button
{:class (stl/css-case :selected selected? :library library-view?)
:ref node-ref
:title (:name file)
:draggable true
:draggable (dm/str (not you-viewer?))
:on-click on-select
:on-key-down handle-key-down
:on-double-click on-navigate
@ -370,6 +372,7 @@
[:& grid-item-library {:file file}]
[:& grid-item-thumbnail
{:file-id (:id file)
:you-viewer? you-viewer?
:revn (:revn file)
:thumbnail-id (:thumbnail-id file)
:background-color (dm/get-in file [:data :options :background])}])
@ -405,6 +408,7 @@
:show? (:menu-open dashboard-local)
:left (+ 24 (:x (:menu-pos dashboard-local)))
:top (:y (:menu-pos dashboard-local))
:you-viewer? you-viewer?
:navigate? true
:on-edit on-edit
:on-menu-close on-menu-close
@ -412,7 +416,7 @@
:parent-id (str file-id "-action-menu")}]])]]]]]))
(mf/defc grid
[{:keys [files project origin limit library-view? create-fn] :as props}]
[{:keys [files project origin limit library-view? create-fn you-viewer?] :as props}]
(let [dragging? (mf/use-state false)
project-id (:id project)
node-ref (mf/use-var nil)
@ -429,11 +433,12 @@
on-drag-enter
(mf/use-fn
(fn [e]
(when (and (not (dnd/has-type? e "penpot/files"))
(or (dnd/has-type? e "Files")
(dnd/has-type? e "application/x-moz-file")))
(dom/prevent-default e)
(reset! dragging? true))))
(when-not you-viewer?
(when (and (not (dnd/has-type? e "penpot/files"))
(or (dnd/has-type? e "Files")
(dnd/has-type? e "application/x-moz-file")))
(dom/prevent-default e)
(reset! dragging? true)))))
on-drag-over
(mf/use-fn
@ -459,6 +464,7 @@
(import-files (.-files (.-dataTransfer e))))))]
[:div {:class (stl/css :dashboard-grid)
:dragabble (dm/str (not you-viewer?))
:on-drag-enter on-drag-enter
:on-drag-over on-drag-over
:on-drag-leave on-drag-leave
@ -480,21 +486,22 @@
:key (:id item)
:navigate? true
:origin origin
:you-viewer? you-viewer?
:library-view? library-view?}])])
:else
[:& empty-placeholder
{:limit limit
:you-viewer? you-viewer?
:create-fn create-fn
:origin origin}])]))
(mf/defc line-grid-row
[{:keys [files selected-files dragging? limit] :as props}]
[{:keys [files selected-files dragging? limit you-viewer?] :as props}]
(let [elements limit
limit (if dragging? (dec limit) limit)]
[:ul
{:class (stl/css :grid-row :no-wrap)
:style {:grid-template-columns (dm/str "repeat(" elements ", 1fr)")}}
[:ul {:class (stl/css :grid-row :no-wrap)
:style {:grid-template-columns (dm/str "repeat(" elements ", 1fr)")}}
(when dragging?
[:li {:class (stl/css :grid-item :dragged)}])
@ -504,11 +511,12 @@
{:id (:id item)
:file item
:selected-files selected-files
:you-viewer? you-viewer?
:key (:id item)
:navigate? false}])]))
(mf/defc line-grid
[{:keys [project team files limit create-fn] :as props}]
[{:keys [project team files limit create-fn you-viewer?] :as props}]
(let [dragging? (mf/use-state false)
project-id (:id project)
team-id (:id team)
@ -527,22 +535,23 @@
on-drag-enter
(mf/use-fn
(mf/deps selected-project)
(mf/deps selected-project you-viewer?)
(fn [e]
(cond
(dnd/has-type? e "penpot/files")
(do
(dom/prevent-default e)
(when-not (or (dnd/from-child? e)
(dnd/broken-event? e))
(when (not= selected-project project-id)
(reset! dragging? true))))
(when-not you-viewer?
(cond
(dnd/has-type? e "penpot/files")
(do
(dom/prevent-default e)
(when-not (or (dnd/from-child? e)
(dnd/broken-event? e))
(when (not= selected-project project-id)
(reset! dragging? true))))
(or (dnd/has-type? e "Files")
(dnd/has-type? e "application/x-moz-file"))
(do
(dom/prevent-default e)
(reset! dragging? true)))))
(or (dnd/has-type? e "Files")
(dnd/has-type? e "application/x-moz-file"))
(do
(dom/prevent-default e)
(reset! dragging? true))))))
on-drag-over
(mf/use-fn
@ -586,6 +595,7 @@
(import-files (.-files (.-dataTransfer e)))))))]
[:div {:class (stl/css :dashboard-grid)
:dragabble (dm/str (not you-viewer?))
:on-drag-enter on-drag-enter
:on-drag-over on-drag-over
:on-drag-leave on-drag-leave
@ -599,10 +609,12 @@
:team-id team-id
:selected-files selected-files
:dragging? @dragging?
:you-viewer? you-viewer?
:limit limit}]
:else
[:& empty-placeholder
{:dragging? @dragging?
:limit limit
:you-viewer? you-viewer?
:create-fn create-fn}])]))

View file

@ -19,7 +19,7 @@
[rumext.v2 :as mf]))
(mf/defc libraries-page
[{:keys [team] :as props}]
[{:keys [team you-viewer?] :as props}]
(let [files-map (mf/deref refs/dashboard-shared-files)
projects (mf/deref refs/dashboard-projects)
@ -56,5 +56,6 @@
:project default-project
:origin :libraries
:limit limit
:you-viewer? you-viewer?
:library-view? components-v2}]]]))

View file

@ -7,13 +7,14 @@
(ns app.main.ui.dashboard.placeholder
(:require-macros [app.main.style :as stl])
(:require
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.icons :as i]
[app.util.i18n :as i18n :refer [tr]]
[rumext.v2 :as mf]))
(mf/defc empty-placeholder
[{:keys [dragging? limit origin create-fn]}]
[{:keys [dragging? limit origin create-fn you-viewer?]}]
(let [on-click
(mf/use-fn
(mf/deps create-fn)
@ -27,14 +28,17 @@
[:li {:class (stl/css :grid-item :grid-empty-placeholder :dragged)}]]
(= :libraries origin)
[:div {:class (stl/css :grid-empty-placeholder :libs)
:data-testid "empty-placeholder"}
[:div {:class (stl/css :text)}
[:> i18n/tr-html* {:content (tr "dashboard.empty-placeholder-drafts")}]]]
[:> empty-placeholder* {:title (tr "dashboard.empty-placeholder-libraries-title")
:type 2
:subtitle (when you-viewer? (tr "dashboard.empty-placeholder-libraries-subtitle-viewer-role"))
:class (stl/css :empty-placeholder-libraries)}
(when-not you-viewer?
[:> i18n/tr-html* {:content (tr "dashboard.empty-placeholder-drafts")
:class (stl/css :placeholder-markdown)
:tag-name "span"}])]
:else
[:div
{:class (stl/css :grid-empty-placeholder)}
[:div {:class (stl/css :grid-empty-placeholder)}
[:button {:class (stl/css :create-new)
:on-click on-click}
i/add]])))

View file

@ -6,6 +6,7 @@
@use "common/refactor/common-refactor.scss" as *;
@use "./grid.scss" as g;
@use "../ds/typography.scss" as t;
.grid-empty-placeholder {
border-radius: $br-12;
@ -89,3 +90,14 @@
font-size: $fs-16;
text-align: center;
}
.placeholder-markdown {
@include t.use-typography("body-large");
a {
color: var(--color-accent-primary);
}
}
.empty-placeholder-libraries {
margin: $s-16;
}

View file

@ -118,9 +118,6 @@
:data-testid "project-delete"})]]
[:*
[:& udi/import-form {:ref file-input
:project-id (:id project)
:on-finish-import on-finish-import}]
[:& context-menu-a11y
{:on-close on-menu-close
:show show?
@ -129,5 +126,8 @@
:top top
:left left
:options options
:workspace false}]]))
:workspace false}]
[:& udi/import-form {:ref file-input
:project-id (:id project)
:on-finish-import on-finish-import}]]))

View file

@ -17,6 +17,7 @@
[app.main.ui.dashboard.inline-edition :refer [inline-edition]]
[app.main.ui.dashboard.pin-button :refer [pin-button*]]
[app.main.ui.dashboard.project-menu :refer [project-menu]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.hooks :as hooks]
[app.main.ui.icons :as i]
[app.util.dom :as dom]
@ -44,15 +45,16 @@
(mf/defc header
{::mf/wrap [mf/memo]}
[]
[{:keys [you-viewer?]}]
(let [on-click (mf/use-fn #(st/emit! (dd/create-project)))]
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
[:div#dashboard-projects-title {:class (stl/css :dashboard-title)}
[:h1 (tr "dashboard.projects-title")]]
[:button {:class (stl/css :btn-secondary :btn-small)
:on-click on-click
:data-testid "new-project-button"}
(tr "dashboard.new-project")]]))
(when-not you-viewer?
[:button {:class (stl/css :btn-secondary :btn-small)
:on-click on-click
:data-testid "new-project-button"}
(tr "dashboard.new-project")])]))
(mf/defc team-hero*
{::mf/wrap [mf/memo]
@ -98,11 +100,14 @@
(l/derived :builtin-templates st/state))
(mf/defc project-item
[{:keys [project first? team files] :as props}]
[{:keys [project first? team files you-viewer?] :as props}]
(let [locale (mf/deref i18n/locale)
file-count (or (:count project) 0)
project-id (:id project)
is-draft-proyect (:is-default project)
team-id (:id team)
empty-state-viewer (and you-viewer?
(= 0 file-count))
dstate (mf/deref refs/dashboard-local)
edit-id (:project-for-edit dstate)
@ -198,7 +203,6 @@
(when (kbd/enter? event)
(on-create-click event))))
handle-menu-click
(mf/use-callback
(mf/deps on-menu-click)
@ -220,20 +224,13 @@
:title (if (:is-default project)
(tr "labels.drafts")
(:name project))
:on-context-menu on-menu-click}
:on-context-menu (when-not you-viewer? on-menu-click)}
(if (:is-default project)
(tr "labels.drafts")
(:name project))])
[:div {:class (stl/css :info-wrapper)}
[:& project-menu
{:project project
:show? (:menu-open @local)
:left (+ 24 (:x (:menu-pos @local)))
:top (:y (:menu-pos @local))
:on-edit on-edit-open
:on-menu-close on-menu-close
:on-import on-import}]
;; We group these two spans under a div to avoid having extra space between them.
[:div
@ -248,29 +245,51 @@
(when-not (:is-default project)
[:> pin-button* {:class (stl/css :pin-button) :is-pinned (:is-pinned project) :on-click toggle-pin :tab-index 0}])
[:button {:class (stl/css :add-file-btn)
:on-click on-create-click
:title (tr "dashboard.new-file")
:aria-label (tr "dashboard.new-file")
:data-testid "project-new-file"
:on-key-down handle-create-click}
add-icon]
(when-not you-viewer?
[:button {:class (stl/css :add-file-btn)
:on-click on-create-click
:title (tr "dashboard.new-file")
:aria-label (tr "dashboard.new-file")
:data-testid "project-new-file"
:on-key-down handle-create-click}
add-icon])
[:button {:class (stl/css :options-btn)
:on-click on-menu-click
:title (tr "dashboard.options")
:aria-label (tr "dashboard.options")
:data-testid "project-options"
:on-key-down handle-menu-click}
menu-icon]]]]]
(when-not you-viewer?
[:button {:class (stl/css :options-btn)
:on-click on-menu-click
:title (tr "dashboard.options")
:aria-label (tr "dashboard.options")
:data-testid "project-options"
:on-key-down handle-menu-click}
menu-icon])]
(when-not you-viewer?
[:& project-menu
{:project project
:show? (:menu-open @local)
:left (+ 24 (:x (:menu-pos @local)))
:top (:y (:menu-pos @local))
:on-edit on-edit-open
:on-menu-close on-menu-close
:on-import on-import}])]]]
[:div {:class (stl/css :grid-container) :ref rowref}
[:& line-grid
{:project project
:team team
:files files
:create-fn create-file
:limit limit}]]
(if empty-state-viewer
[:> empty-placeholder* {:title (if is-draft-proyect
(tr "dashboard.empty-placeholder-drafts-title")
(tr "dashboard.empty-placeholder-files-title"))
:class (stl/css :placeholder-placement)
:type 1
:subtitle (if is-draft-proyect
(tr "dashboard.empty-placeholder-drafts-subtitle")
(tr "dashboard.empty-placeholder-files-subtitle"))}]
[:& line-grid
{:project project
:team team
:files files
:create-fn create-file
:you-viewer? you-viewer?
:limit limit}])]
(when (and (> limit 0)
(> file-count limit))
@ -295,6 +314,7 @@
recent-map (mf/deref recent-files-ref)
you-owner? (get-in team [:permissions :is-owner])
you-admin? (get-in team [:permissions :is-admin])
you-viewer? (not (get-in team [:permissions :can-edit]))
can-invite? (or you-owner? you-admin?)
show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true))
@ -327,7 +347,7 @@
(when (seq projects)
[:*
[:& header]
[:& header {:you-viewer? you-viewer?}]
[:div {:class (stl/css :projects-container)}
[:*
(when (and show-team-hero?
@ -350,5 +370,6 @@
[:& project-item {:project project
:team team
:files files
:you-viewer? you-viewer?
:first? (= project (first projects))
:key id}]))]]]])))

View file

@ -128,6 +128,10 @@
padding: 0 $s-4;
}
.placeholder-placement {
margin: $s-16 $s-32;
}
.show-more {
--show-more-color: var(--button-secondary-foreground-color-rest);
@include buttonStyle;

View file

@ -118,13 +118,10 @@
(defn get-available-roles
[permissions]
(->> [{:value "editor" :label (tr "labels.editor")}
(->> [{:value "viewer" :label (tr "labels.viewer")}
{:value "editor" :label (tr "labels.editor")}
(when (:is-admin permissions)
{:value "admin" :label (tr "labels.admin")})
;; Temporarily disabled viewer roles
;; https://tree.taiga.io/project/penpot/issue/1083
;; {:value "viewer" :label (tr "labels.viewer")}
]
{:value "admin" :label (tr "labels.admin")})]
(filterv identity)))
(def ^:private schema:invite-member-form
@ -146,7 +143,7 @@
team-id (:id team)
initial (mf/with-memo [team-id]
{:role "editor" :team-id team-id})
{:role "viewer" :team-id team-id})
form (fm/use-form :schema schema:invite-member-form
:initial initial)
@ -256,7 +253,7 @@
(mf/defc rol-info
{::mf/wrap-props false}
[{:keys [member team on-set-admin on-set-editor on-set-owner profile]}]
[{:keys [member team on-set-admin on-set-editor on-set-owner on-set-viewer profile]}]
(let [member-is-owner? (:is-owner member)
member-is-admin? (and (:is-admin member) (not member-is-owner?))
member-is-editor? (and (:can-edit member) (and (not member-is-admin?) (not member-is-owner?)))
@ -294,12 +291,12 @@
[:li {:on-click on-set-editor
:class (stl/css :rol-dropdown-item)}
(tr "labels.editor")]
;; Temporarily disabled viewer role
;; https://tree.taiga.io/project/penpot/issue/1083
;; [:li {:on-click set-viewer} (tr "labels.viewer")]
[:li {:on-click on-set-viewer
:class (stl/css :rol-dropdown-item)}
(tr "labels.viewer")]
(when you-owner?
[:li {:on-click (partial on-set-owner member)
:class (:stl/css :rol-dropdown-item)}
:class (stl/css :rol-dropdown-item)}
(tr "labels.owner")])]]]))
(mf/defc member-actions
@ -344,6 +341,7 @@
(let [member-id (:id member)
on-set-admin (mf/use-fn (mf/deps member-id) (partial set-role! member-id :admin))
on-set-editor (mf/use-fn (mf/deps member-id) (partial set-role! member-id :editor))
on-set-viewer (mf/use-fn (mf/deps member-id) (partial set-role! member-id :viewer))
owner? (dm/get-in team [:permissions :is-owner])
on-set-owner
@ -459,6 +457,7 @@
:team team
:on-set-admin on-set-admin
:on-set-editor on-set-editor
:on-set-viewer on-set-viewer
:on-set-owner on-set-owner
:profile profile}]]
@ -567,7 +566,11 @@
[:li {:data-role "editor"
:class (stl/css :rol-dropdown-item)
:on-click on-change'}
(tr "labels.editor")]]]]))
(tr "labels.editor")]
[:li {:data-role "viewer"
:class (stl/css :rol-dropdown-item)
:on-click on-change'}
(tr "labels.viewer")]]]]))
(mf/defc invitation-actions
{::mf/wrap-props false}

View file

@ -18,6 +18,7 @@
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[app.main.ui.ds.layout.tab-switcher :refer [tab-switcher*]]
[app.main.ui.ds.notifications.toast :refer [toast*]]
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
[app.main.ui.ds.product.loader :refer [loader*]]
[app.main.ui.ds.storybook :as sb]
[app.util.i18n :as i18n]))
@ -32,6 +33,7 @@
:Icon icon*
:IconButton icon-button*
:Input input*
:EmptyPlaceholder empty-placeholder*
:Loader loader*
:RawSvg raw-svg*
:Select select*

View file

@ -10,5 +10,8 @@
$sz-16: px2rem(16);
$sz-32: px2rem(32);
$sz-36: px2rem(36);
$sz-160: px2rem(160);
$sz-200: px2rem(200);
$sz-224: px2rem(224);
$sz-400: px2rem(400);
$sz-964: px2rem(964);

View file

@ -25,6 +25,10 @@
(def ^:svg-id marketing-layers "marketing-layers")
(def ^:svg-id penpot-logo "penpot-logo")
(def ^:svg-id penpot-logo-icon "penpot-logo-icon")
(def ^:svg-id empty-placeholder-1-left "empty-placeholder-1-left")
(def ^:svg-id empty-placeholder-1-right "empty-placeholder-1-right")
(def ^:svg-id empty-placeholder-2-left "empty-placeholder-2-left")
(def ^:svg-id empty-placeholder-2-right "empty-placeholder-2-right")
(def raw-svg-list "A collection of all raw SVG assets" (collect-raw-svgs))

View file

@ -12,8 +12,6 @@
[app.main.ui.ds.foundations.assets.icon :as i]
[rumext.v2 :as mf]))
(def levels (set '("info" "warning" "error" "success")))
(def ^:private icons-by-level
{"info" i/info
"warning" i/msg-neutral

View file

@ -0,0 +1,40 @@
;; This Source Code Form is subject to the terms of the Mozilla Public
;; License, v. 2.0. If a copy of the MPL was not distributed with this
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
;;
;; Copyright (c) KALEIDOS INC
(ns app.main.ui.ds.product.empty-placeholder
(:require-macros
[app.common.data.macros :as dm]
[app.main.style :as stl])
(:require
[app.main.ui.ds.foundations.assets.raw-svg :refer [raw-svg*]]
[app.main.ui.ds.foundations.typography :as t]
[app.main.ui.ds.foundations.typography.text :refer [text*]]
[rumext.v2 :as mf]))
(def ^:private schema:empty-placeholder
[:map
[:class {:optional true} :string]
[:title :string]
[:subtitle {:optional true} [:maybe :string]]
[:type {:optional true} [:maybe [:enum 1 2]]]])
(mf/defc empty-placeholder*
{::mf/props :obj
::mf/schema schema:empty-placeholder}
[{:keys [class title subtitle type children] :rest props}]
(let [class (dm/str class " " (stl/css :empty-placeholder))
props (mf/spread-props props {:class class})
type (or type 1)
decoration-type (dm/str "empty-placeholder-" (str type))]
[:> "div" props
[:> raw-svg* {:id (dm/str decoration-type "-left") :class (stl/css :svg-decor)}]
[:div {:class (stl/css :text-wrapper)}
[:> text* {:as "span" :typography t/title-medium :class (stl/css :placeholder-title)} title]
(when subtitle
[:> text* {:as "span" :typography t/body-large} subtitle])
children]
[:> raw-svg* {:id (dm/str decoration-type "-right") :class (stl/css :svg-decor)}]]))

View file

@ -0,0 +1,38 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
//
// Copyright (c) KALEIDOS INC
@use "../_sizes.scss" as *;
@use "../_borders.scss" as *;
.empty-placeholder {
display: grid;
grid-template-columns: auto 1fr auto;
place-content: center;
background: none;
color: var(--color-foreground-secondary);
height: $sz-160;
max-width: $sz-964;
border-radius: $br-8;
border: $b-1 solid var(--color-background-quaternary);
}
.text-wrapper {
display: grid;
grid-auto-rows: auto;
align-self: center;
justify-self: center;
max-width: $sz-400;
}
.placeholder-title {
color: var(--color-foreground-primary);
}
.svg-decor {
height: $sz-160;
width: $sz-200;
color: var(--color-background-quaternary);
}

View file

@ -0,0 +1,33 @@
import * as React from "react";
import Components from "@target/components";
const { EmptyPlaceholder } = Components;
export default {
title: "Product/EmptyPlaceholder",
component: EmptyPlaceholder,
argTypes: {
title: {
control: { type: "text" },
},
type: {
control: "radio",
options: [1, 2],
},
},
args: {
type: 1,
title: "Lorem ipsum",
subtitle:
"dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.",
},
render: ({ ...args }) => <EmptyPlaceholder {...args} />,
};
export const Default = {};
export const AlternativeDecoration = {
args: {
type: 2,
},
};

View file

@ -61,14 +61,16 @@
(defn- get-available-roles
[]
[{:value "editor" :label (tr "labels.editor")}
[{:value "viewer" :label (tr "labels.viewer")}
{:value "editor" :label (tr "labels.editor")}
{:value "admin" :label (tr "labels.admin")}])
(mf/defc team-form-step-2
{::mf/props :obj}
[{:keys [name on-back go-to-team?]}]
(let [initial (mf/with-memo []
{:role "editor" :name name})
(let [initial (mf/use-memo
#(do {:role "viewer"
:name name}))
form (fm/use-form :schema schema:invite-form
:initial initial)

View file

@ -181,9 +181,9 @@
:id "prototype"
:content interactions-content}
#js {:label (tr "workspace.options.inspect")
:id "inspect"
:content inspect-content}]]
#js {:label (tr "workspace.options.inspect")
:id "inspect"
:content inspect-content}]]
[:div {:class (stl/css :tool-window)}
[:> tab-switcher* {:tabs tabs

View file

@ -62,8 +62,6 @@
on-change
(mf/use-fn
(fn [new-color old-color from-picker?]
(prn "new-color" new-color)
(prn "old-color" old-color)
(let [old-color (-> old-color
(dissoc :name :path)
(d/without-nils))

View file

@ -205,7 +205,8 @@
(fn [event]
(st/emit! (dw/create-page {:file-id file-id :project-id project-id}))
(-> event dom/get-current-target dom/blur!)))
read-only? (mf/use-ctx ctx/workspace-read-only?)]
read-only? (mf/use-ctx ctx/workspace-read-only?)
user-viewer? (mf/use-ctx ctx/user-viewer?)]
[:div {:class (stl/css :sitemap)
:style #js {"--height" (str size "px")}}
@ -218,9 +219,10 @@
:class (stl/css :title-spacing-sitemap)}
(if ^boolean read-only?
[:& badge-notification {:is-focus true
:size :small
:content (tr "labels.view-only")}]
(when (not ^boolean user-viewer?)
[:& badge-notification {:is-focus true
:size :small
:content (tr "labels.view-only")}])
[:button {:class (stl/css :add-page)
:on-click on-create}
i/add])]