0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-04-04 11:01:20 -05:00

Add improved abstraction for team permissions

Relevant changes:
- replace user-viewer? with can-edit removing many double
  negations on the code
- always use team permissions making the permissions access uniform
  around all the code
- expose team permissions to ui tree through ctx/team-permissions
  context
This commit is contained in:
Andrey Antukh 2024-10-18 16:23:24 +02:00 committed by Alonso Torres
parent b3fcbd91e4
commit d6da8afdce
29 changed files with 412 additions and 391 deletions

View file

@ -178,15 +178,14 @@
(ptk/reify ::commit-changes
ptk/WatchEvent
(watch [_ state _]
(let [file-id (or file-id (:current-file-id state))
uchg (vec undo-changes)
rchg (vec redo-changes)
features (features/get-team-enabled-features state)
user-viewer? (not (dm/get-in state [:workspace-file :permissions :can-edit]))]
(let [file-id (or file-id (:current-file-id state))
uchg (vec undo-changes)
rchg (vec redo-changes)
features (features/get-team-enabled-features state)
permissions (:permissions state)]
;; Prevent commit changes by a viewer team member (it really should never happen)
(if user-viewer?
(rx/empty)
(when (:can-edit permissions)
(rx/of (-> params
(assoc :undo-group undo-group)
(assoc :features features)

View file

@ -7,10 +7,11 @@
(ns app.main.data.common
"A general purpose events."
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.schema :as sm]
[app.common.types.components-list :as ctkl]
[app.common.types.team :as tt]
[app.common.types.team :as ctt]
[app.config :as cf]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
@ -196,7 +197,7 @@
(rx/tap on-success)
(rx/catch on-error))))))
(defn- change-role-msg
(defn- get-change-role-msg
[role]
(case role
:viewer (tr "dashboard.permissions-change.viewer")
@ -204,26 +205,23 @@
:admin (tr "dashboard.permissions-change.admin")
:owner (tr "dashboard.permissions-change.owner")))
(defn change-team-permissions
[{:keys [team-id role workspace?]}]
(defn change-team-role
[{:keys [team-id role]}]
(dm/assert! (uuid? team-id))
(dm/assert! (contains? tt/valid-roles role))
(ptk/reify ::change-team-permissions
(dm/assert! (contains? ctt/valid-roles role))
(ptk/reify ::change-team-role
ptk/WatchEvent
(watch [_ _ _]
(rx/of (ntf/info (change-role-msg role))))
(rx/of (ntf/info (get-change-role-msg role))))
ptk/UpdateEvent
(update [_ state]
(let [route (if workspace?
[:workspace-file :permissions]
[:teams team-id :permissions])]
(update-in state route
(fn [permissions]
(merge permissions (get tt/permissions-for-role role))))))))
(let [permissions (get ctt/permissions-for-role role)]
(-> state
(update :permissions merge permissions)
(update-in [:team :permissions] merge permissions)
(d/update-in-when [:teams team-id :permissions] merge permissions))))))
(defn team-membership-change
[{:keys [team-id team-name change]}]
@ -232,12 +230,12 @@
ptk/WatchEvent
(watch [_ state _]
(when (= :removed change)
(let [msg (tr "dashboard.removed-from-team" team-name)]
(let [message (tr "dashboard.removed-from-team" team-name)
profile (:profile state)]
(rx/concat
(rx/of (rt/nav :dashboard-projects {:team-id (get-in state [:profile :default-team-id])}))
(->> (rx/of (ntf/info msg))
;; Delay so the navigation can finish
(rx/of (rt/nav :dashboard-projects {:team-id (:default-team-id profile)}))
(->> (rx/of (ntf/info message))
;; Delay so the navigation can finish
(rx/delay 250))))))))

View file

@ -12,7 +12,7 @@
[app.common.files.helpers :as cfh]
[app.common.logging :as log]
[app.common.schema :as sm]
[app.common.types.team :as tt]
[app.common.types.team :as ctt]
[app.common.uri :as u]
[app.common.uuid :as uuid]
[app.config :as cf]
@ -482,7 +482,7 @@
(defn update-team-member-role
[{:keys [role member-id] :as params}]
(dm/assert! (uuid? member-id))
(dm/assert! (contains? tt/valid-roles role))
(dm/assert! (contains? ctt/valid-roles role))
(ptk/reify ::update-team-member-role
ptk/WatchEvent
@ -605,7 +605,7 @@
(sm/check-email! email))
(dm/assert! (uuid? team-id))
(dm/assert! (contains? tt/valid-roles role))
(dm/assert! (contains? ctt/valid-roles role))
(ptk/reify ::update-team-invitation-role
IDeref
@ -1211,19 +1211,18 @@
;; Notifications
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn- handle-change-team-permissions-dashboard
[msg]
(ptk/reify ::handle-change-team-permissions-dashboard
(defn- handle-change-team-role
[params]
(ptk/reify ::handle-change-team-role
ptk/WatchEvent
(watch [_ _ _]
(rx/of (dc/change-team-permissions (assoc msg :workspace? false))
(rx/of (dc/change-team-role params)
(modal/hide)))))
(defn- process-message
[{:keys [type] :as msg}]
(case type
:notification (dc/handle-notification msg)
:team-role-change (handle-change-team-permissions-dashboard msg)
:team-role-change (handle-change-team-role msg)
:team-membership-change (dc/team-membership-change msg)
nil))
nil))

View file

@ -97,6 +97,7 @@
(update [_ state]
(-> state
(assoc :team team)
(assoc :permissions (:permissions team))
(assoc :current-team-id (:id team))))
ptk/WatchEvent

View file

@ -68,7 +68,7 @@
(ptk/reify ::set-workspace-read-only
ptk/UpdateEvent
(update [_ state]
(assoc-in state [:workspace-global :read-only?] read-only?))
(update state :workspace-global assoc :read-only? read-only?))
ptk/WatchEvent
(watch [_ _ _]

View file

@ -97,26 +97,22 @@
(rx/concat stream (rx/of (dws/send endmsg)))))))
(defn- handle-change-team-permissions
(defn- handle-change-team-role
[{:keys [role] :as msg}]
(ptk/reify ::handle-change-team-permissions
(ptk/reify ::handle-change-team-role
ptk/WatchEvent
(watch [_ _ _]
(let [viewer? (= :viewer role)]
(rx/concat
(rx/of :interrupt
(dwe/clear-edition-mode)
(dwc/set-workspace-read-only false))
(->> (rx/of (dc/change-team-permissions msg))
;; Delay so anything that launched :interrupt can finish
(rx/delay 100))
(if viewer?
(rx/of (modal/hide)
(dwly/set-options-mode :inspect))
(rx/of (dwly/set-options-mode :design))))))))
(rx/concat
(rx/of :interrupt
(dwe/clear-edition-mode)
(dwc/set-workspace-read-only false))
(->> (rx/of (dc/change-team-role msg))
;; Delay so anything that launched :interrupt can finish
(rx/delay 100))
(if (= :viewer role)
(rx/of (modal/hide)
(dwly/set-options-mode :inspect))
(rx/of (dwly/set-options-mode :design)))))))
(defn- process-message
[{:keys [type] :as msg}]
@ -129,7 +125,7 @@
:file-change (handle-file-change msg)
:library-change (handle-library-change msg)
:notification (dc/handle-notification msg)
:team-role-change (handle-change-team-permissions (assoc msg :workspace? true))
:team-role-change (handle-change-team-role msg)
:team-membership-change (dc/team-membership-change msg)
nil))
@ -284,4 +280,4 @@
(watch [_ state _]
(when (contains? (:workspace-libraries state) file-id)
(rx/of (dwl/ext-library-changed file-id modified-at revn changes)
(dwl/notify-sync-file file-id))))))
(dwl/notify-sync-file file-id))))))

View file

@ -37,18 +37,16 @@
;; Shortcuts
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
(defn toggle-layout-flag
(defn- toggle-layout-flag
[flag]
(-> (dw/toggle-layout-flag flag)
(vary-meta assoc ::ev/origin "workspace-shortcuts")))
(defn emit-when-no-readonly
(defn- emit-when-no-readonly
[& events]
(let [file (deref refs/workspace-file)
user-viewer? (not (dm/get-in file [:permissions :can-edit]))
read-only? (or (deref refs/workspace-read-only?)
user-viewer?)]
(when-not read-only?
(let [can-edit? (:can-edit (deref refs/permissions))
read-only? (deref refs/workspace-read-only?)]
(when (and can-edit? (not read-only?))
(run! st/emit! events))))
(def esc-pressed

View file

@ -7,7 +7,7 @@
(ns app.main.data.workspace.text.shortcuts
(:require
[app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.files.helpers :as cfh]
[app.common.text :as txt]
[app.main.data.shortcuts :as ds]
[app.main.data.workspace.texts :as dwt]
@ -189,17 +189,20 @@
props)))
(defn- update-attrs-when-no-readonly [props]
(let [undo-id (js/Symbol)
file (deref refs/workspace-file)
user-viewer? (not (dm/get-in file [:permissions :can-edit]))
read-only? (or (deref refs/workspace-read-only?)
user-viewer?)
shapes-with-children (deref refs/selected-shapes-with-children)
text-shapes (filter #(= (:type %) :text) shapes-with-children)
props (if (> (count text-shapes) 1)
(blend-props text-shapes props)
props)]
(when (and (not read-only?) text-shapes)
(let [undo-id (js/Symbol)
can-edit? (:can-edit (deref refs/permissions))
read-only? (deref refs/workspace-read-only?)
text-shapes (->> (deref refs/selected-shapes-with-children)
(filter cfh/text-shape?)
(not-empty))
props (if (> (count text-shapes) 1)
(blend-props text-shapes props)
props)]
(when (and can-edit? (not read-only?) text-shapes)
(st/emit! (dwu/start-undo-transaction undo-id))
(run! #(update-attrs % props) text-shapes)
(st/emit! (dwu/commit-undo-transaction undo-id)))))

View file

@ -27,6 +27,12 @@
(def profile
(l/derived :profile st/state))
(def team
(l/derived :team st/state))
(def permissions
(l/derived :permissions st/state))
(def teams
(l/derived :teams st/state))

View file

@ -32,4 +32,4 @@
(def is-component? (mf/create-context false))
(def sidebar (mf/create-context nil))
(def user-viewer? (mf/create-context nil))
(def team-permissions (mf/create-context nil))

View file

@ -61,14 +61,15 @@
(mf/defc dashboard-content
[{:keys [team projects project section search-term profile invite-email] :as props}]
(let [container (mf/use-ref)
content-width (mf/use-state 0)
project-id (:id project)
team-id (:id team)
you-viewer? (not (dm/get-in team [:permissions :can-edit]))
(let [container (mf/use-ref)
content-width (mf/use-state 0)
project-id (:id project)
team-id (:id team)
dashboard-local (mf/deref refs/dashboard-local)
file-menu-open? (:menu-open dashboard-local)
permissions (:permissions team)
dashboard-local (mf/deref refs/dashboard-local)
file-menu-open? (:menu-open dashboard-local)
default-project-id
(mf/with-memo [projects]
@ -87,8 +88,9 @@
(mf/use-fn
#(st/emit! (dd/clear-selected-files)))
show-templates (and (contains? cf/flags :dashboard-templates-section)
(not you-viewer?))]
show-templates
(and (contains? cf/flags :dashboard-templates-section)
(not (:can-edit permissions)))]
(mf/with-effect []
(let [key1 (events/listen js/window "resize" on-resize)]
@ -117,7 +119,7 @@
:content-width @content-width}])]
:dashboard-fonts
[:& fonts-page {:team team :you-viewer? you-viewer?}]
[:& fonts-page {:team team}]
:dashboard-font-providers
[:& font-providers-page {:team team}]
@ -125,7 +127,7 @@
:dashboard-files
(when project
[:*
[:& files-section {:team team :project project :you-viewer? you-viewer?}]
[:& files-section {:team team :project project}]
(when show-templates
[:& templates-section {:profile profile
:team-id team-id
@ -138,7 +140,7 @@
:search-term search-term}]
:dashboard-libraries
[:& libraries-page {:team team :you-viewer? you-viewer?}]
[:& libraries-page {:team team}]
:dashboard-team-members
[:& team-members-page {:team team :profile profile :invite-email invite-email}]
@ -231,8 +233,7 @@
invite-email (-> route :query-params :invite-email)
teams (mf/deref refs/teams)
team (get teams team-id)
team (mf/deref refs/team)
projects (mf/deref refs/dashboard-projects)
project (get projects project-id)
@ -261,29 +262,30 @@
[:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-project-id) {:value project-id}
;; NOTE: dashboard events and other related functions assumes
;; that the team is a implicit context variable that is
;; available using react context or accessing
;; the :current-team-id on the state. We set the key to the
;; team-id because we want to completely refresh all the
;; components on team change. Many components assumes that the
;; team is already set so don't put the team into mf/deps.
(when (and team initialized?)
[:main {:class (stl/css :dashboard)
:key (:id team)}
[:& sidebar
{:team team
:projects projects
:project project
:profile profile
:section section
:search-term search-term}]
(when (and team profile (seq projects))
[:& dashboard-content
{:projects projects
:profile profile
:project project
:section section
:search-term search-term
:team team
:invite-email invite-email}])])]]))
[:& (mf/provider ctx/team-permissions) {:value (:permissions team)}
;; NOTE: dashboard events and other related functions assumes
;; that the team is a implicit context variable that is
;; available using react context or accessing
;; the :current-team-id on the state. We set the key to the
;; team-id because we want to completely refresh all the
;; components on team change. Many components assumes that the
;; team is already set so don't put the team into mf/deps.
(when (and team initialized?)
[:main {:class (stl/css :dashboard)
:key (:id team)}
[:& sidebar
{:team team
:projects projects
:project project
:profile profile
:section section
:search-term search-term}]
(when (and team profile (seq projects))
[:& dashboard-content
{:projects projects
:profile profile
:project project
:section section
:search-term search-term
:team team
:invite-email invite-email}])])]]]))

View file

@ -57,7 +57,7 @@
(mf/defc file-menu
{::mf/wrap-props false}
[{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id you-viewer?]}]
[{:keys [files show? on-edit on-menu-close top left navigate? origin parent-id can-edit]}]
(assert (seq files) "missing `files` prop")
(assert (boolean? show?) "missing `show?` prop")
(assert (fn? on-edit) "missing `on-edit` prop")
@ -245,13 +245,12 @@
options
(if multi?
[(when-not you-viewer?
[(when can-edit
{:name (tr "dashboard.duplicate-multi" file-count)
:id "duplicate-multi"
:handler on-duplicate})
(when (and (or (seq current-projects) (seq other-teams))
(not you-viewer?))
(when (and (or (seq current-projects) (seq other-teams)) can-edit)
{:name (tr "dashboard.move-to-multi" file-count)
:id "file-move-multi"
:options sub-options})
@ -269,14 +268,12 @@
:id "file-standard-export-multi"
:handler on-export-standard-files}
(when (and (:is-shared file)
(not you-viewer?))
(when (and (:is-shared file) can-edit)
{:name (tr "labels.unpublish-multi-files" file-count)
:id "file-unpublish-multi"
:handler on-del-shared})
(when (and (not is-lib-page?)
(not you-viewer?))
(when (and (not is-lib-page?) can-edit)
{:name :separator}
{:name (tr "labels.delete-multi-files" file-count)
:id "file-delete-multi"
@ -285,14 +282,12 @@
[{:name (tr "dashboard.open-in-new-tab")
:id "file-open-new-tab"
:handler on-new-tab}
(when (and (not is-search-page?)
(not you-viewer?))
(when (and (not is-search-page?) can-edit)
{:name (tr "labels.rename")
:id "file-rename"
:handler on-edit})
(when (and (not is-search-page?)
(not you-viewer?))
(when (and (not is-search-page?) can-edit)
{:name (tr "dashboard.duplicate")
:id "file-duplicate"
:handler on-duplicate})
@ -300,13 +295,13 @@
(when (and (not is-lib-page?)
(not is-search-page?)
(or (seq current-projects) (seq other-teams))
(not you-viewer?))
can-edit)
{:name (tr "dashboard.move-to")
:id "file-move-to"
:options sub-options})
(when (and (not is-search-page?)
(not you-viewer?))
can-edit)
(if (:is-shared file)
{:name (tr "dashboard.unpublish-shared")
:id "file-del-shared"
@ -330,10 +325,10 @@
:id "download-standard-file"
:handler on-export-standard-files}
(when (and (not is-lib-page?) (not is-search-page?) (not you-viewer?))
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name :separator})
(when (and (not is-lib-page?) (not is-search-page?) (not you-viewer?))
(when (and (not is-lib-page?) (not is-search-page?) can-edit)
{:name (tr "labels.delete")
:id "file-delete"
:handler on-delete})])]

View file

@ -28,8 +28,10 @@
(def ^:private menu-icon
(i/icon-xref :menu (stl/css :menu-icon)))
(mf/defc header
[{:keys [project create-fn you-viewer?] :as props}]
(mf/defc header*
{::mf/props :obj
::mf/private true}
[{:keys [project create-fn can-edit]}]
(let [local (mf/use-state
{:menu-open false
:edition false})
@ -72,8 +74,7 @@
[:div#dashboard-drafts-title {:class (stl/css :dashboard-title)}
[:h1 (tr "labels.drafts")]]
(if (and (:edition @local)
(not you-viewer?))
(if (and (:edition @local) can-edit)
[:& inline-edition
{:content (:name project)
:on-end (fn [name]
@ -89,7 +90,7 @@
(:name project)]]))
[:div {:class (stl/css :dashboard-header-actions)}
(when-not you-viewer?
(when ^boolean can-edit
[:a {:class (stl/css :btn-secondary :btn-small :new-file)
:tab-index "0"
:on-click on-create-click
@ -106,7 +107,7 @@
:on-click toggle-pin
:on-key-down (fn [event] (when (kbd/enter? event) (toggle-pin event)))}])
(when-not you-viewer?
(when ^boolean can-edit
[:div {:class (stl/css :icon)
:tab-index "0"
:on-click on-menu-click
@ -116,7 +117,7 @@
(on-menu-click event)))}
menu-icon])
(when-not you-viewer?
(when ^boolean can-edit
[:& project-menu {:project project
:show? (:menu-open @local)
:left (- (:x (:menu-pos @local)) 180)
@ -126,9 +127,11 @@
:on-import on-import}])]]))
(mf/defc files-section
[{:keys [project team you-viewer?] :as props}]
(let [files-map (mf/deref refs/dashboard-files)
project-id (:id project)
{::mf/props :obj}
[{:keys [project team]}]
(let [files-map (mf/deref refs/dashboard-files)
can-edit? (-> team :permissions :can-edit)
project-id (:id project)
is-draft-proyect (:is-default project)
[rowref limit] (hooks/use-dynamic-grid-item-width)
@ -139,7 +142,7 @@
(sort-by :modified-at)
(reverse)))
file-count (or (count files) 0)
empty-state-viewer (and you-viewer?
empty-state-viewer (and (not can-edit?)
(= 0 file-count))
on-file-created
@ -171,10 +174,10 @@
(dd/clear-selected-files)))
[:*
[:& header {:team team
:project project
:you-viewer? you-viewer?
:create-fn create-file}]
[:> header* {:team team
:can-edit can-edit?
:project project
:create-fn create-file}]
[:section {:class (stl/css :dashboard-container :no-bg)
:ref rowref}
(if empty-state-viewer
@ -188,7 +191,7 @@
(tr "dashboard.empty-placeholder-files-subtitle"))}]
[:& grid {:project project
:files files
:you-viewer? you-viewer?
:can-edit can-edit?
:origin :files
:create-fn create-file
:limit limit}])]]))

View file

@ -269,7 +269,7 @@
{::mf/props :obj
::mf/private true
::mf/memo true}
[{:keys [font-id variants you-viewer?]}]
[{:keys [font-id variants can-edit]}]
(let [font (first variants)
menu-open* (mf/use-state false)
@ -361,11 +361,11 @@
[:div {:class (stl/css :table-field :variants)}
(for [{:keys [id] :as item} variants]
[:div {:class (stl/css-case :variant true
:inhert-variant you-viewer?)
:inhert-variant (not can-edit))
:key (dm/str id)}
[:span {:class (stl/css :label)}
[:& font-variant-display-name {:variant item}]]
(when-not you-viewer?
(when can-edit
[:span
{:class (stl/css :icon :close)
:data-id (dm/str id)
@ -384,7 +384,7 @@
:on-click on-cancel}
i/close]]
(when-not you-viewer?
(when can-edit
[:div {:class (stl/css :table-field :options)}
[:span {:class (stl/css :icon)
:on-click on-menu-open}
@ -397,7 +397,7 @@
:on-edit on-edit}]]))]))
(mf/defc installed-fonts
[{:keys [fonts you-viewer?] :as props}]
[{:keys [fonts can-edit] :as props}]
(let [sterm (mf/use-state "")
matches?
@ -426,7 +426,7 @@
(group-by :font-id))]
[:& installed-font {:key (dm/str font-id "-installed")
:font-id font-id
:you-viewer? you-viewer?
:can-edit can-edit
:variants variants}])]
(nil? fonts)
@ -435,27 +435,33 @@
[:div {:class (stl/css :label)} (tr "dashboard.loading-fonts")]]
:else
(if you-viewer?
[:> empty-placeholder* {:title (tr "dashboard.fonts.empty-placeholder-viewer")
:subtitle (tr "dashboard.fonts.empty-placeholder-viewer-sub")
:type 2}]
(if ^boolean can-edit
[:div {:class (stl/css :fonts-placeholder)}
[:div {:class (stl/css :icon)} i/text]
[:div {:class (stl/css :label)} (tr "dashboard.fonts.empty-placeholder")]]))]))
[:div {:class (stl/css :label)} (tr "dashboard.fonts.empty-placeholder")]]
[:> empty-placeholder*
{:title (tr "dashboard.fonts.empty-placeholder-viewer")
:subtitle (tr "dashboard.fonts.empty-placeholder-viewer-sub")
:type 2}]))]))
(mf/defc fonts-page
[{:keys [team you-viewer?] :as props}]
(let [fonts (mf/deref refs/dashboard-fonts)]
{::mf/props :obj}
[{:keys [team]}]
(let [fonts (mf/deref refs/dashboard-fonts)
permissions (:permissions team)
can-edit (:can-edit permissions)]
[:*
[:& header {:team team :section :fonts}]
[:section {:class (stl/css :dashboard-container :dashboard-fonts)}
(when-not you-viewer?
(when ^boolean can-edit
[:& uploaded-fonts {:team team :installed-fonts fonts}])
[:& installed-fonts {:team team :fonts fonts :you-viewer? you-viewer?}]]]))
[:& installed-fonts {:team team :fonts fonts :can-edit can-edit}]]]))
(mf/defc font-providers-page
[{:keys [team] :as props}]
{::mf/props :obj}
[{:keys [team]}]
[:*
[:& header {:team team :section :providers}]
[:section {:class (stl/css :dashboard-container)}

View file

@ -73,7 +73,7 @@
(mf/defc grid-item-thumbnail
{::mf/wrap-props false}
[{:keys [file-id revn thumbnail-id background-color you-viewer?]}]
[{:keys [file-id revn thumbnail-id background-color can-edit]}]
(let [container (mf/use-ref)
visible? (h/use-visible container :once? true)]
@ -94,12 +94,12 @@
(when visible?
(if thumbnail-id
[:img {:class (stl/css :grid-item-thumbnail-image)
:draggable (dm/str (not you-viewer?))
:draggable (dm/str can-edit)
:src (cf/resolve-media thumbnail-id)
:loading "lazy"
:decoding "async"}]
[:> loader* {:class (stl/css :grid-loader)
:draggable (dm/str (not you-viewer?))
:draggable (dm/str can-edit)
:overlay true
:title (tr "labels.loading")}]))]))
@ -233,7 +233,7 @@
(mf/defc grid-item
{:wrap [mf/memo]}
[{:keys [file origin library-view? you-viewer?] :as props}]
[{:keys [file origin library-view? can-edit] :as props}]
(let [file-id (:id file)
;; FIXME: this breaks react hooks rule, hooks should never to
@ -276,10 +276,10 @@
on-drag-start
(mf/use-fn
(mf/deps selected-files you-viewer?)
(mf/deps selected-files can-edit)
(fn [event]
(st/emit! (dd/hide-file-menu))
(when-not you-viewer?
(when can-edit
(let [offset (dom/get-offset-position (.-nativeEvent event))
select-current? (not (contains? selected-files (:id file)))
@ -359,7 +359,7 @@
{:class (stl/css-case :selected selected? :library library-view?)
:ref node-ref
:title (:name file)
:draggable (dm/str (not you-viewer?))
:draggable (dm/str can-edit)
:on-click on-select
:on-key-down handle-key-down
:on-double-click on-navigate
@ -372,7 +372,7 @@
[:& grid-item-library {:file file}]
[:& grid-item-thumbnail
{:file-id (:id file)
:you-viewer? you-viewer?
:can-edit can-edit
:revn (:revn file)
:thumbnail-id (:thumbnail-id file)
:background-color (dm/get-in file [:data :options :background])}])
@ -408,7 +408,7 @@
:show? (:menu-open dashboard-local)
:left (+ 24 (:x (:menu-pos dashboard-local)))
:top (:y (:menu-pos dashboard-local))
:you-viewer? you-viewer?
:can-edit can-edit
:navigate? true
:on-edit on-edit
:on-menu-close on-menu-close
@ -416,7 +416,7 @@
:parent-id (str file-id "-action-menu")}]])]]]]]))
(mf/defc grid
[{:keys [files project origin limit library-view? create-fn you-viewer?] :as props}]
[{:keys [files project origin limit library-view? create-fn can-edit] :as props}]
(let [dragging? (mf/use-state false)
project-id (:id project)
node-ref (mf/use-var nil)
@ -433,7 +433,7 @@
on-drag-enter
(mf/use-fn
(fn [e]
(when-not you-viewer?
(when can-edit
(when (and (not (dnd/has-type? e "penpot/files"))
(or (dnd/has-type? e "Files")
(dnd/has-type? e "application/x-moz-file")))
@ -464,7 +464,7 @@
(import-files (.-files (.-dataTransfer e))))))]
[:div {:class (stl/css :dashboard-grid)
:dragabble (dm/str (not you-viewer?))
:dragabble (dm/str can-edit)
:on-drag-enter on-drag-enter
:on-drag-over on-drag-over
:on-drag-leave on-drag-leave
@ -486,18 +486,18 @@
:key (:id item)
:navigate? true
:origin origin
:you-viewer? you-viewer?
:can-edit can-edit
:library-view? library-view?}])])
:else
[:& empty-placeholder
{:limit limit
:you-viewer? you-viewer?
:can-edit can-edit
:create-fn create-fn
:origin origin}])]))
(mf/defc line-grid-row
[{:keys [files selected-files dragging? limit you-viewer?] :as props}]
[{:keys [files selected-files dragging? limit can-edit] :as props}]
(let [elements limit
limit (if dragging? (dec limit) limit)]
[:ul {:class (stl/css :grid-row :no-wrap)
@ -511,12 +511,12 @@
{:id (:id item)
:file item
:selected-files selected-files
:you-viewer? you-viewer?
:can-edit can-edit
:key (:id item)
:navigate? false}])]))
(mf/defc line-grid
[{:keys [project team files limit create-fn you-viewer?] :as props}]
[{:keys [project team files limit create-fn can-edit] :as props}]
(let [dragging? (mf/use-state false)
project-id (:id project)
team-id (:id team)
@ -535,9 +535,9 @@
on-drag-enter
(mf/use-fn
(mf/deps selected-project you-viewer?)
(mf/deps selected-project can-edit)
(fn [e]
(when-not you-viewer?
(when can-edit
(cond
(dnd/has-type? e "penpot/files")
(do
@ -595,7 +595,7 @@
(import-files (.-files (.-dataTransfer e)))))))]
[:div {:class (stl/css :dashboard-grid)
:dragabble (dm/str (not you-viewer?))
:dragabble (dm/str can-edit)
:on-drag-enter on-drag-enter
:on-drag-over on-drag-over
:on-drag-leave on-drag-leave
@ -609,12 +609,12 @@
:team-id team-id
:selected-files selected-files
:dragging? @dragging?
:you-viewer? you-viewer?
:can-edit can-edit
:limit limit}]
:else
[:& empty-placeholder
{:dragging? @dragging?
:limit limit
:you-viewer? you-viewer?
:can-edit can-edit
:create-fn create-fn}])]))

View file

@ -19,9 +19,11 @@
[rumext.v2 :as mf]))
(mf/defc libraries-page
[{:keys [team you-viewer?] :as props}]
{::mf/props :obj}
[{:keys [team] :as props}]
(let [files-map (mf/deref refs/dashboard-shared-files)
projects (mf/deref refs/dashboard-projects)
can-edit (-> team :permissions :can-edit)
default-project (->> projects vals (d/seek :is-default))
@ -56,6 +58,6 @@
:project default-project
:origin :libraries
:limit limit
:you-viewer? you-viewer?
:can-edit can-edit
:library-view? components-v2}]]]))

View file

@ -14,7 +14,7 @@
[rumext.v2 :as mf]))
(mf/defc empty-placeholder
[{:keys [dragging? limit origin create-fn you-viewer?]}]
[{:keys [dragging? limit origin create-fn can-edit]}]
(let [on-click
(mf/use-fn
(mf/deps create-fn)
@ -28,11 +28,14 @@
[:li {:class (stl/css :grid-item :grid-empty-placeholder :dragged)}]]
(= :libraries origin)
[:> 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?
[:> empty-placeholder*
{:title (tr "dashboard.empty-placeholder-libraries-title")
:type 2
:subtitle (when-not can-edit
(tr "dashboard.empty-placeholder-libraries-subtitle-viewer-role"))
:class (stl/css :empty-placeholder-libraries)}
(when can-edit
[:> i18n/tr-html* {:content (tr "dashboard.empty-placeholder-libraries")
:class (stl/css :placeholder-markdown)
:tag-name "span"}])]

View file

@ -7,7 +7,6 @@
(ns app.main.ui.dashboard.projects
(:require-macros [app.main.style :as stl])
(:require
[app.common.data.macros :as dm]
[app.common.geom.point :as gpt]
[app.main.data.dashboard :as dd]
[app.main.data.events :as ev]
@ -46,12 +45,12 @@
(mf/defc header
{::mf/wrap [mf/memo]}
[{:keys [you-viewer?]}]
[{:keys [can-edit]}]
(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")]]
(when-not you-viewer?
(when can-edit
[:button {:class (stl/css :btn-secondary :btn-small)
:on-click on-click
:data-testid "new-project-button"}
@ -101,13 +100,13 @@
(l/derived :builtin-templates st/state))
(mf/defc project-item
[{:keys [project first? team files you-viewer?] :as props}]
[{:keys [project first? team files can-edit] :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?
empty-state-viewer (and (not can-edit)
(= 0 file-count))
dstate (mf/deref refs/dashboard-local)
@ -225,7 +224,7 @@
:title (if (:is-default project)
(tr "labels.drafts")
(:name project))
:on-context-menu (when-not you-viewer? on-menu-click)}
:on-context-menu (when can-edit on-menu-click)}
(if (:is-default project)
(tr "labels.drafts")
(:name project))])
@ -246,7 +245,7 @@
(when-not (:is-default project)
[:> pin-button* {:class (stl/css :pin-button) :is-pinned (:is-pinned project) :on-click toggle-pin :tab-index 0}])
(when-not you-viewer?
(when ^boolean can-edit
[:button {:class (stl/css :add-file-btn)
:on-click on-create-click
:title (tr "dashboard.new-file")
@ -255,7 +254,7 @@
:on-key-down handle-create-click}
add-icon])
(when-not you-viewer?
(when ^boolean can-edit
[:button {:class (stl/css :options-btn)
:on-click on-menu-click
:title (tr "dashboard.options")
@ -263,7 +262,8 @@
:data-testid "project-options"
:on-key-down handle-menu-click}
menu-icon])]
(when-not you-viewer?
(when ^boolean can-edit
[:& project-menu
{:project project
:show? (:menu-open @local)
@ -289,7 +289,7 @@
:team team
:files files
:create-fn create-file
:you-viewer? you-viewer?
:can-edit can-edit
:limit limit}])]
(when (and (> limit 0)
@ -309,14 +309,16 @@
(mf/defc projects-section
[{:keys [team projects profile] :as props}]
(let [projects (->> (vals projects)
(sort-by :modified-at)
(reverse))
recent-map (mf/deref recent-files-ref)
you-owner? (dm/get-in team [:permissions :is-owner])
you-admin? (dm/get-in team [:permissions :is-admin])
you-viewer? (not (dm/get-in team [:permissions :can-edit]))
can-invite? (or you-owner? you-admin?)
permisions (:permissions team)
can-edit (:can-edit permisions)
can-invite (or (:is-owner permisions)
(:is-admin permisions))
show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true))
show-team-hero? (deref show-team-hero*)
@ -348,11 +350,11 @@
(when (seq projects)
[:*
[:& header {:you-viewer? you-viewer?}]
[:& header {:can-edit can-edit}]
[:div {:class (stl/css :projects-container)}
[:*
(when (and show-team-hero?
can-invite?
can-invite
(not is-defalt-team?))
[:> team-hero* {:team team :on-close on-close}])
@ -362,7 +364,7 @@
:with-team-hero (and (not is-my-penpot)
(not is-defalt-team?)
show-team-hero?
can-invite?))}
can-invite))}
(for [{:keys [id] :as project} projects]
(let [files (when recent-map
(->> (vals recent-map)
@ -371,6 +373,6 @@
[:& project-item {:project project
:team team
:files files
:you-viewer? you-viewer?
:can-edit can-edit
:first? (= project (first projects))
:key id}]))]]]])))

View file

@ -255,28 +255,30 @@
(mf/defc rol-info
{::mf/wrap-props false}
[{: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?)))
show? (mf/use-state false)
(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)))
show? (mf/use-state false)
you-owner? (dm/get-in team [:permissions :is-owner])
you-admin? (dm/get-in team [:permissions :is-admin])
is-you? (= (:id profile) (:id member))
permissions (:permissions team)
is-owner (:is-owner permissions)
is-admin (:is-admin permissions)
can-change-rol? (or you-owner? you-admin?)
not-superior? (or you-owner? (and can-change-rol? (or member-is-admin? member-is-editor?)))
is-you (= (:id profile) (:id member))
role (cond
member-is-owner? "labels.owner"
member-is-admin? "labels.admin"
member-is-editor? "labels.editor"
:else "labels.viewer")
can-change-rol (or is-owner is-admin)
not-superior (or is-admin (and can-change-rol (or member-is-admin member-is-editor)))
on-show (mf/use-fn #(reset! show? true))
on-hide (mf/use-fn #(reset! show? false))]
role (cond
member-is-owner "labels.owner"
member-is-admin "labels.admin"
member-is-editor "labels.editor"
:else "labels.viewer")
on-show (mf/use-fn #(reset! show? true))
on-hide (mf/use-fn #(reset! show? false))]
[:*
(if (and can-change-rol? not-superior? (not (and is-you? you-owner?)))
(if (and can-change-rol not-superior (not (and is-you is-owner)))
[:div {:class (stl/css :rol-selector :has-priv)
:on-click on-show}
[:span {:class (stl/css :rol-label)} (tr role)]
@ -295,7 +297,7 @@
[:li {:on-click on-set-viewer
:class (stl/css :rol-dropdown-item)}
(tr "labels.viewer")]
(when you-owner?
(when is-owner
[:li {:on-click (partial on-set-owner member)
:class (stl/css :rol-dropdown-item)}
(tr "labels.owner")])]]]))
@ -320,7 +322,6 @@
:on-click on-show}
menu-icon]
[:& dropdown {:show @show? :on-close on-hide}
[:ul {:class (stl/css :actions-dropdown)}
(when is-you?
@ -910,12 +911,13 @@
(tr "dashboard.webhooks.create")]])
(mf/defc webhook-actions
{::mf/wrap-props false}
[{:keys [on-edit on-delete can-edit?]}]
{::mf/props :obj
::mf/private true}
[{:keys [on-edit on-delete can-edit]}]
(let [show? (mf/use-state false)
on-show (mf/use-fn #(reset! show? true))
on-hide (mf/use-fn #(reset! show? false))]
(if can-edit?
(if can-edit
[:*
[:button {:class (stl/css :menu-btn)
:on-click on-show}
@ -948,7 +950,7 @@
creator-id (:profile-id webhook)
profile (mf/deref refs/profile)
user-id (:id profile)
can-edit? (or (:can-edit permissions)
can-edit (or (:can-edit permissions)
(= creator-id user-id))
on-edit
(mf/use-fn
@ -1002,8 +1004,8 @@
[:div {:class (stl/css :table-field :actions)}
[:& webhook-actions
{:on-edit on-edit
:can-edit? can-edit?
:on-delete on-delete}]]]))
:on-delete on-delete
:can-edit can-edit}]]]))
(mf/defc webhooks-list
{::mf/wrap-props false}
@ -1053,9 +1055,9 @@
stats (mf/deref refs/dashboard-team-stats)
you-owner? (get-in team [:permissions :is-owner])
you-admin? (get-in team [:permissions :is-admin])
can-edit? (or you-owner? you-admin?)
permissions (:permissions team)
can-edit (or (:is-owner permissions)
(:is-admin permissions))
on-image-click
(mf/use-callback #(dom/click (mf/ref-val finput)))
@ -1086,13 +1088,13 @@
[:div {:class (stl/css :block-text)}
(:name team)]
[:div {:class (stl/css :team-icon)}
(when can-edit?
(when can-edit
[:button {:class (stl/css :update-overlay)
:on-click on-image-click}
image-icon])
[:img {:class (stl/css :team-image)
:src (cfg/resolve-team-photo-url team)}]
(when can-edit?
(when can-edit
[:& file-uploader {:accept "image/jpeg,image/png"
:multi false
:ref finput

View file

@ -68,9 +68,8 @@
(mf/defc team-form-step-2
{::mf/props :obj}
[{:keys [name on-back go-to-team?]}]
(let [initial (mf/use-memo
#(do {:role "editor"
:name name}))
(let [initial (mf/with-memo []
{:role "editor" :name name})
form (fm/use-form :schema schema:invite-form
:initial initial)

View file

@ -165,16 +165,16 @@
(let [layout (mf/deref refs/workspace-layout)
wglobal (mf/deref refs/workspace-global)
team (mf/deref refs/team)
file (mf/deref refs/workspace-file)
project (mf/deref refs/workspace-project)
team-id (:team-id project)
file-name (:name file)
permissions (:permissions team)
user-viewer? (not (dm/get-in file [:permissions :can-edit]))
read-only? (or (mf/deref refs/workspace-read-only?)
user-viewer?)
read-only? (mf/deref refs/workspace-read-only?)
read-only? (or read-only? (not (:can-edit permissions)))
file-ready* (mf/with-memo [file-id]
(make-file-ready-ref file-id))
@ -214,7 +214,7 @@
[:& (mf/provider ctx/current-page-id) {:value page-id}
[:& (mf/provider ctx/components-v2) {:value components-v2?}
[:& (mf/provider ctx/workspace-read-only?) {:value read-only?}
[:& (mf/provider ctx/user-viewer?) {:value user-viewer?}
[:& (mf/provider ctx/team-permissions) {:value permissions}
[:section {:class (stl/css :workspace)
:style {:background-color background-color
:touch-action "none"}}

View file

@ -534,15 +534,17 @@
[:& menu-entry {:title (tr "workspace.assets.duplicate")
:on-click do-duplicate}]]))
(mf/defc viewport-context-menu
[{:keys [read-only?]}]
(mf/defc viewport-context-menu*
{::mf/props :obj}
[]
(let [focus (mf/deref refs/workspace-focus-selected)
read-only? (mf/use-ctx ctx/workspace-read-only?)
do-paste #(st/emit! (dw/paste-from-clipboard))
do-hide-ui #(st/emit! (-> (dw/toggle-layout-flag :hide-ui)
(vary-meta assoc ::ev/origin "workspace-context-menu")))
do-toggle-focus-mode #(st/emit! (dw/toggle-focus-mode))]
[:*
(when-not read-only?
(when-not ^boolean read-only?
[:& menu-entry {:title (tr "workspace.shape.menu.paste")
:shortcut (sc/get-tooltip :paste)
:on-click do-paste}])
@ -640,26 +642,26 @@
:disabled (and (not single?) (not can-merge?))}]]))
;; FIXME: optimize because it is rendered always
(mf/defc context-menu
[]
(let [mdata (mf/deref menu-ref)
top (- (get-in mdata [:position :y]) 20)
left (get-in mdata [:position :x])
dropdown-ref (mf/use-ref)
read-only? (mf/use-ctx ctx/workspace-read-only?)]
(let [mdata (mf/deref menu-ref)
top (- (get-in mdata [:position :y]) 20)
left (get-in mdata [:position :x])
dropdown-ref (mf/use-ref)
read-only? (mf/use-ctx ctx/workspace-read-only?)]
(mf/use-effect
(mf/deps mdata)
#(let [dropdown (mf/ref-val dropdown-ref)]
(when dropdown
(let [bounding-rect (dom/get-bounding-rect dropdown)
window-size (dom/get-window-size)
delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0)
delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0)
new-style (str "top: " (- top delta-y) "px; "
"left: " (- left delta-x) "px;")]
(when (or (> delta-x 0) (> delta-y 0))
(.setAttribute ^js dropdown "style" new-style))))))
(mf/with-effect [mdata]
(when-let [dropdown (mf/ref-val dropdown-ref)]
(let [bounding-rect (dom/get-bounding-rect dropdown)
window-size (dom/get-window-size)
delta-x (max (- (+ (:right bounding-rect) 250) (:width window-size)) 0)
delta-y (max (- (:bottom bounding-rect) (:height window-size)) 0)
new-style (str "top: " (- top delta-y) "px; "
"left: " (- left delta-x) "px;")]
(when (or (> delta-x 0) (> delta-y 0))
(.setAttribute ^js dropdown "style" new-style)))))
[:& dropdown {:show (boolean mdata)
:on-close #(st/emit! dw/hide-context-menu)}
@ -669,11 +671,11 @@
:on-context-menu prevent-default}
[:ul {:class (stl/css :context-list)}
(if read-only?
[:& viewport-context-menu {:mdata mdata :read-only? read-only?}]
(if ^boolean read-only?
[:> viewport-context-menu* {:mdata mdata}]
(case (:kind mdata)
:shape [:& shape-context-menu {:mdata mdata}]
:page [:& page-item-context-menu {:mdata mdata}]
:grid-track [:& grid-track-context-menu {:mdata mdata}]
:grid-cells [:& grid-cells-context-menu {:mdata mdata}]
[:& viewport-context-menu {:mdata mdata}]))]]]))
[:& viewport-context-menu* {:mdata mdata}]))]]]))

View file

@ -42,8 +42,9 @@
;; --- Header menu and submenus
(mf/defc help-info-menu
{::mf/wrap-props false
(mf/defc help-info-menu*
{::mf/props :obj
::mf/private true
::mf/wrap [mf/memo]}
[{:keys [layout on-close]}]
(let [nav-to-helpc-center
@ -172,8 +173,9 @@
[:span {:class (stl/css-case :feedback true
:item-name true)} (tr "labels.give-feedback")]])]))
(mf/defc preferences-menu
{::mf/wrap-props false
(mf/defc preferences-menu*
{::mf/props :obj
::mf/private true
::mf/wrap [mf/memo]}
[{:keys [layout profile toggle-flag on-close toggle-theme]}]
(let [show-nudge-options (mf/use-fn #(modal/show! {:type :nudge-option}))]
@ -283,8 +285,9 @@
(for [sc (scd/split-sc (sc/get-tooltip :toggle-theme))]
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]]))
(mf/defc view-menu
{::mf/wrap-props false
(mf/defc view-menu*
{::mf/props :obj
::mf/private true
::mf/wrap [mf/memo]}
[{:keys [layout toggle-flag on-close]}]
(let [read-only? (mf/use-ctx ctx/workspace-read-only?)
@ -412,13 +415,17 @@
(for [sc (scd/split-sc (sc/get-tooltip :hide-ui))]
[:span {:class (stl/css :shortcut-key) :key sc} sc])]]]))
(mf/defc edit-menu
{::mf/wrap-props false
(mf/defc edit-menu*
{::mf/props :obj
::mf/private true
::mf/wrap [mf/memo]}
[{:keys [on-close user-viewer?]}]
[{:keys [on-close]}]
(let [select-all (mf/use-fn #(st/emit! (dw/select-all)))
undo (mf/use-fn #(st/emit! dwu/undo))
redo (mf/use-fn #(st/emit! dwu/redo))]
redo (mf/use-fn #(st/emit! dwu/redo))
perms (mf/use-ctx ctx/team-permissions)
can-edit (:can-edit perms)]
[:& dropdown-menu {:show true
:list-class (stl/css-case :sub-menu true
:edit true)
@ -439,38 +446,39 @@
:key sc}
sc])]]
(when-not :user-viewer? user-viewer?
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click undo
:on-key-down (fn [event]
(when (kbd/enter? event)
(undo event)))
:id "file-menu-undo"}
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.undo")]
[:span {:class (stl/css :shortcut)}
(for [sc (scd/split-sc (sc/get-tooltip :undo))]
[:span {:class (stl/css :shortcut-key)
:key sc}
sc])]])
(when can-edit
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click undo
:on-key-down (fn [event]
(when (kbd/enter? event)
(undo event)))
:id "file-menu-undo"}
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.undo")]
[:span {:class (stl/css :shortcut)}
(for [sc (scd/split-sc (sc/get-tooltip :undo))]
[:span {:class (stl/css :shortcut-key)
:key sc}
sc])]])
(when-not :user-viewer? user-viewer?
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click redo
:on-key-down (fn [event]
(when (kbd/enter? event)
(redo event)))
:id "file-menu-redo"}
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.redo")]
[:span {:class (stl/css :shortcut)}
(when can-edit
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click redo
:on-key-down (fn [event]
(when (kbd/enter? event)
(redo event)))
:id "file-menu-redo"}
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.redo")]
[:span {:class (stl/css :shortcut)}
(for [sc (scd/split-sc (sc/get-tooltip :redo))]
[:span {:class (stl/css :shortcut-key)
:key sc}
sc])]])]))
(for [sc (scd/split-sc (sc/get-tooltip :redo))]
[:span {:class (stl/css :shortcut-key)
:key sc}
sc])]])]))
(mf/defc file-menu
{::mf/wrap-props false}
[{:keys [on-close file user-viewer?]}]
(mf/defc file-menu*
{::mf/props :obj
::mf/private true}
[{:keys [on-close file can-edit]}]
(let [file-id (:id file)
shared? (:is-shared file)
@ -564,7 +572,7 @@
:id "file-menu-remove-shared"}
[:span {:class (stl/css :item-name)} (tr "dashboard.unpublish-shared")]]
(when-not user-viewer?
(when can-edit
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click on-add-shared
:on-key-down on-add-shared-key-down
@ -613,8 +621,9 @@
[:span {:class (stl/css :item-name)}
(tr "dashboard.export-frames")]])]))
(mf/defc plugins-menu
{::mf/wrap-props false
(mf/defc plugins-menu*
{::mf/props :obj
::mf/private true
::mf/wrap [mf/memo]}
[{:keys [open-plugins on-close]}]
(when (features/active-feature? @st/state "plugins/runtime")
@ -659,15 +668,13 @@
[:span {:class (stl/css :item-name)} name]])])))
(mf/defc menu
{::mf/wrap-props false}
{::mf/props :obj}
[{:keys [layout file profile]}]
(let [show-menu* (mf/use-state false)
show-menu? (deref show-menu*)
sub-menu* (mf/use-state false)
sub-menu (deref sub-menu*)
user-viewer? (mf/use-ctx ctx/user-viewer?)
open-menu
(mf/use-fn
(fn [event]
@ -814,24 +821,21 @@
(case sub-menu
:file
[:& file-menu
{:file file
:on-close close-sub-menu
:user-viewer? user-viewer?}]
[:> file-menu* {:file file
:on-close close-sub-menu}]
:edit
[:& edit-menu
{:on-close close-sub-menu
:user-viewer? user-viewer?}]
[:> edit-menu*
{:on-close close-sub-menu}]
:view
[:& view-menu
[:> view-menu*
{:layout layout
:toggle-flag toggle-flag
:on-close close-sub-menu}]
:preferences
[:& preferences-menu
[:> preferences-menu*
{:layout layout
:profile profile
:toggle-flag toggle-flag
@ -839,12 +843,12 @@
:on-close close-sub-menu}]
:plugins
[:& plugins-menu
[:& plugins-menu*
{:open-plugins open-plugins-manager
:on-close close-sub-menu}]
:help-info
[:& help-info-menu
[:& help-info-menu*
{:layout layout
:on-close close-sub-menu}]

View file

@ -134,8 +134,7 @@
::mf/props :obj}
[{:keys [selected shapes shapes-with-children page-id file-id on-change-section on-expand]}]
(let [objects (mf/deref refs/workspace-page-objects)
user-viewer? (mf/use-ctx ctx/user-viewer?)
permissions (mf/use-ctx ctx/team-permissions)
selected-shapes (into [] (keep (d/getf objects)) selected)
first-selected-shape (first selected-shapes)
@ -176,10 +175,7 @@
tabs
(if user-viewer?
#js [#js {:label (tr "workspace.options.inspect")
:id "inspect"
:content inspect-content}]
(if (:can-edit permissions)
#js [#js {:label (tr "workspace.options.design")
:id "design"
:content design-content}
@ -189,6 +185,9 @@
:content interactions-content}
#js {:label (tr "workspace.options.inspect")
:id "inspect"
:content inspect-content}]
#js [#js {:label (tr "workspace.options.inspect")
:id "inspect"
:content inspect-content}])]

View file

@ -30,11 +30,11 @@
(mf/defc page-item
{::mf/wrap-props false}
[{:keys [page index deletable? selected? editing? hovering?]}]
(let [input-ref (mf/use-ref)
id (:id page)
delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id)))
navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dw/go-to-page id)))
workspace-read-only? (mf/use-ctx ctx/workspace-read-only?)
(let [input-ref (mf/use-ref)
id (:id page)
delete-fn (mf/use-fn (mf/deps id) #(st/emit! (dw/delete-page id)))
navigate-fn (mf/use-fn (mf/deps id) #(st/emit! :interrupt (dw/go-to-page id)))
read-only? (mf/use-ctx ctx/workspace-read-only?)
on-delete
(mf/use-fn
@ -47,11 +47,11 @@
on-double-click
(mf/use-fn
(mf/deps workspace-read-only?)
(mf/deps read-only?)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(when-not workspace-read-only?
(when-not read-only?
(st/emit! (dw/start-rename-page-item id)))))
on-blur
@ -86,15 +86,15 @@
:data {:id id
:index index
:name (:name page)}
:draggable? (not workspace-read-only?))
:draggable? (not read-only?))
on-context-menu
(mf/use-fn
(mf/deps id workspace-read-only?)
(mf/deps id read-only?)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(when-not workspace-read-only?
(when-not read-only?
(let [position (dom/get-client-position event)]
(st/emit! (dw/show-page-item-context-menu
{:position position
@ -147,7 +147,7 @@
[:span {:class (stl/css :page-name) :data-testid "page-name"}
(:name page)]
[:div {:class (stl/css :page-actions)}
(when (and deletable? (not workspace-read-only?))
(when (and deletable? (not read-only?))
[:button {:on-click on-delete}
i/delete])]])]]))
@ -206,8 +206,7 @@
(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?)
user-viewer? (mf/use-ctx ctx/user-viewer?)]
permissions (mf/use-ctx ctx/team-permissions)]
[:div {:class (stl/css :sitemap)
:style #js {"--height" (str size "px")}}
@ -220,7 +219,7 @@
:class (stl/css :title-spacing-sitemap)}
(if ^boolean read-only?
(when (not ^boolean user-viewer?)
(when ^boolean (:can-edit permissions)
[:& badge-notification {:is-focus true
:size :small
:content (tr "labels.view-only")}])

View file

@ -95,8 +95,11 @@
vbox' (mf/use-debounce 100 vbox)
permissions (mf/use-ctx ctx/team-permissions)
read-only? (mf/use-ctx ctx/workspace-read-only?)
;; DEREFS
user-viewer? (mf/use-ctx ctx/user-viewer?)
drawing (mf/deref refs/workspace-drawing)
focus (mf/deref refs/workspace-focus-selected)
@ -169,12 +172,11 @@
text-editing? (and edition (= :text (get-in base-objects [edition :type])))
grid-editing? (and edition (ctl/grid-layout? base-objects edition))
workspace-read-only? (mf/use-ctx ctx/workspace-read-only?)
mode-inspect? (= options-mode :inspect)
on-click (actions/on-click hover selected edition drawing-path? drawing-tool space? selrect z?)
on-context-menu (actions/on-context-menu hover hover-ids workspace-read-only?)
on-double-click (actions/on-double-click hover hover-ids hover-top-frame-id drawing-path? base-objects edition drawing-tool z? workspace-read-only?)
on-context-menu (actions/on-context-menu hover hover-ids read-only?)
on-double-click (actions/on-double-click hover hover-ids hover-top-frame-id drawing-path? base-objects edition drawing-tool z? read-only?)
comp-inst-ref (mf/use-ref false)
on-drag-enter (actions/on-drag-enter comp-inst-ref)
@ -182,19 +184,19 @@
on-drag-end (actions/on-drag-over comp-inst-ref)
on-drop (actions/on-drop file comp-inst-ref)
on-pointer-down (actions/on-pointer-down @hover selected edition drawing-tool text-editing? node-editing? grid-editing?
drawing-path? create-comment? space? panning z? workspace-read-only?)
drawing-path? create-comment? space? panning z? read-only?)
on-pointer-up (actions/on-pointer-up disable-paste)
on-pointer-enter (actions/on-pointer-enter in-viewport?)
on-pointer-leave (actions/on-pointer-leave in-viewport?)
on-pointer-move (actions/on-pointer-move move-stream)
on-move-selected (actions/on-move-selected hover hover-ids selected space? z? workspace-read-only?)
on-menu-selected (actions/on-menu-selected hover hover-ids selected workspace-read-only?)
on-move-selected (actions/on-move-selected hover hover-ids selected space? z? read-only?)
on-menu-selected (actions/on-menu-selected hover hover-ids selected read-only?)
on-frame-enter (actions/on-frame-enter frame-hover)
on-frame-leave (actions/on-frame-leave frame-hover)
on-frame-select (actions/on-frame-select selected workspace-read-only?)
on-frame-select (actions/on-frame-select selected read-only?)
disable-events? (contains? layout :comments)
show-comments? (= drawing-tool :comments)
@ -267,9 +269,9 @@
rule-area-size (/ rulers/ruler-area-size zoom)]
(hooks/setup-dom-events zoom disable-paste in-viewport? workspace-read-only? drawing-tool drawing-path?)
(hooks/setup-dom-events zoom disable-paste in-viewport? read-only? drawing-tool drawing-path?)
(hooks/setup-viewport-size vport viewport-ref)
(hooks/setup-cursor cursor alt? mod? space? panning drawing-tool drawing-path? node-editing? z? workspace-read-only?)
(hooks/setup-cursor cursor alt? mod? space? panning drawing-tool drawing-path? node-editing? z? read-only?)
(hooks/setup-keyboard alt? mod? space? z? shift?)
(hooks/setup-hover-shapes page-id move-stream base-objects transform selected mod? hover measure-hover
hover-ids hover-top-frame-id @hover-disabled? focus zoom show-measures?)
@ -278,7 +280,7 @@
(hooks/setup-active-frames base-objects hover-ids selected active-frames zoom transform vbox)
[:div {:class (stl/css :viewport) :style #js {"--zoom" zoom} :data-testid "viewport"}
(when-not user-viewer?
(when (:can-edit permissions)
[:& top-bar/top-bar {:layout layout}])
[:div {:class (stl/css :viewport-overlays)}
;; The behaviour inside a foreign object is a bit different that in plain HTML so we wrap
@ -288,7 +290,7 @@
[:div {:style {:pointer-events (when-not (dbg/enabled? :html-text) "none")
;; some opacity because to debug auto-width events will fill the screen
:opacity 0.6}}
(when-not workspace-read-only?
(when (and (:can-edit permissions) (not read-only?))
[:& stvh/viewport-texts
{:key (dm/str "texts-" page-id)
:page-id page-id

View file

@ -41,11 +41,11 @@
(defn on-pointer-down
[{:keys [id blocked hidden type]} selected edition drawing-tool text-editing?
node-editing? grid-editing? drawing-path? create-comment? space? panning z? workspace-read-only?]
node-editing? grid-editing? drawing-path? create-comment? space? panning z? read-only?]
(mf/use-callback
(mf/deps id blocked hidden type selected edition drawing-tool text-editing?
node-editing? grid-editing? drawing-path? create-comment? @z? @space?
panning workspace-read-only?)
panning read-only?)
(fn [bevent]
;; We need to handle editor related stuff here because
;; handling on editor dom node does not works properly.
@ -101,24 +101,24 @@
(cond
node-editing?
;; Handle path node area selection
(when-not workspace-read-only?
(when-not read-only?
(st/emit! (dwdp/handle-area-selection shift?)))
drawing-tool
(when-not workspace-read-only?
(when-not read-only?
(st/emit! (dd/start-drawing drawing-tool)))
(or (not id) mod?)
(st/emit! (dw/handle-area-selection shift?))
(not drawing-tool)
(when-not workspace-read-only?
(when-not read-only?
(st/emit! (dw/start-move-selected id shift?)))))))))))))
(defn on-move-selected
[hover hover-ids selected space? z? workspace-read-only?]
[hover hover-ids selected space? z? read-only?]
(mf/use-callback
(mf/deps @hover @hover-ids selected @space? @z? workspace-read-only?)
(mf/deps @hover @hover-ids selected @space? @z? read-only?)
(fn [bevent]
(let [event (.-nativeEvent bevent)
shift? (kbd/shift? event)
@ -132,20 +132,20 @@
(dom/prevent-default bevent)
(dom/stop-propagation bevent)
(when-not (or workspace-read-only? @z?)
(when-not (or read-only? @z?)
(st/emit! (dw/start-move-selected))))))))
(defn on-frame-select
[selected workspace-read-only?]
[selected read-only?]
(mf/use-callback
(mf/deps selected workspace-read-only?)
(mf/deps selected read-only?)
(fn [event id]
(let [shift? (kbd/shift? event)
selected? (contains? selected id)
selected-drawtool (deref refs/selected-drawing-tool)]
(st/emit! (when (or shift? (not selected?))
(dw/select-shape id shift?))
(when (and (nil? selected-drawtool) (not shift?) (not workspace-read-only?))
(when (and (nil? selected-drawtool) (not shift?) (not read-only?))
(dw/start-move-selected)))))))
(defn on-frame-enter
@ -195,10 +195,10 @@
(st/emit! (dw/increase-zoom pt)))))))))
(defn on-double-click
[hover hover-ids hover-top-frame-id drawing-path? objects edition drawing-tool z? workspace-read-only?]
[hover hover-ids hover-top-frame-id drawing-path? objects edition drawing-tool z? read-only?]
(mf/use-callback
(mf/deps @hover @hover-ids @hover-top-frame-id drawing-path? edition drawing-tool @z? workspace-read-only?)
(mf/deps @hover @hover-ids @hover-top-frame-id drawing-path? edition drawing-tool @z? read-only?)
(fn [event]
(dom/stop-propagation event)
(when-not @z?
@ -223,7 +223,7 @@
(fn []
(when (and (not drawing-path?) shape)
(cond
(and editable? (not= id edition) (not workspace-read-only?))
(and editable? (not= id edition) (not read-only?))
(st/emit! (dw/select-shape id)
(dw/start-editing-selected))
@ -231,16 +231,16 @@
(do (reset! hover selected-shape)
(st/emit! (dw/select-shape (:id selected-shape))))
(and (not selected-shape) (some? grid-layout-id) (not workspace-read-only?))
(and (not selected-shape) (some? grid-layout-id) (not read-only?))
(st/emit! (dw/start-edition-mode grid-layout-id)))))))))))
(defn on-context-menu
[hover hover-ids workspace-read-only?]
[hover hover-ids read-only?]
(mf/use-fn
(mf/deps @hover @hover-ids workspace-read-only?)
(mf/deps @hover @hover-ids read-only?)
(fn [event]
(dom/prevent-default event)
;;(when-not workspace-read-only?
;;(when-not read-only?
(when (or (dom/class? (dom/get-target event) "viewport-controls")
(dom/child? (dom/get-target event) (dom/query ".grid-layout-editor"))
(dom/class? (dom/get-target event) "viewport-selrect"))
@ -248,20 +248,20 @@
;; Delayed callback because we need to wait to the previous context menu to be closed
(ts/schedule
#(st/emit!
(if (and (not workspace-read-only?) (some? @hover))
(if (and (not read-only?) (some? @hover))
(dw/show-shape-context-menu {:position position
:shape @hover
:hover-ids @hover-ids})
(dw/show-context-menu {:position position})))))))))
(defn on-menu-selected
[hover hover-ids selected workspace-read-only?]
[hover hover-ids selected read-only?]
(mf/use-callback
(mf/deps @hover @hover-ids selected workspace-read-only?)
(mf/deps @hover @hover-ids selected read-only?)
(fn [event]
(dom/prevent-default event)
(dom/stop-propagation event)
(when-not workspace-read-only?
(when-not read-only?
(let [position (dom/get-client-position event)]
(st/emit! (dw/show-shape-context-menu {:position position :hover-ids @hover-ids})))))))
@ -538,14 +538,14 @@
(st/emit! (dwm/upload-media-workspace params))))))))
(defn on-paste
[disable-paste in-viewport? workspace-read-only?]
[disable-paste in-viewport? read-only?]
(mf/use-fn
(mf/deps workspace-read-only?)
(mf/deps read-only?)
(fn [event]
;; We disable the paste just after mouse-up of a middle button so
;; when panning won't paste the content into the workspace
(let [tag-name (-> event dom/get-target dom/get-tag-name)]
(when (and (not (#{"INPUT" "TEXTAREA"} tag-name))
(not @disable-paste)
(not workspace-read-only?))
(not read-only?))
(st/emit! (dw/paste-from-event event @in-viewport?)))))))

View file

@ -39,7 +39,7 @@
:pages []
:pages-index {}}
:workspace-libraries {}
:features/team #{"components/v2"}})
:features-team #{"components/v2"}})
(def ^:private idmap (atom {}))

View file

@ -20,7 +20,7 @@
:current-page-id nil
:workspace-data nil
:workspace-libraries {}
:features/team #{"components/v2"}})
:features-team #{"components/v2"}})
(defn- on-error
[cause]
@ -33,6 +33,7 @@
(let [state (-> initial-state
(assoc :current-file-id (:id file)
:current-page-id (cthf/current-page-id file)
:permissions {:can-edit true}
:workspace-file (dissoc file :data)
:workspace-data (:data file)))
store (ptk/store {:state state :on-error on-error})]