0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-03-10 14:51:37 -05:00

Add viewer role to workspace

This commit is contained in:
Pablo Alba 2024-09-27 17:02:39 +02:00
parent cf150891df
commit 226ab7233b
17 changed files with 187 additions and 121 deletions

View file

@ -12,6 +12,8 @@
### :sparkles: New features
- Viewer role for team members [Taiga #1056 & #6590](https://tree.taiga.io/project/penpot/us/1056 & https://tree.taiga.io/project/penpot/us/6590)
### :bug: Bugs fixed
## 2.3.0

View file

@ -791,7 +791,7 @@
[:map
[:id ::sm/uuid]
[:fullname :string]]]
[:role [::sm/one-of valid-roles]]
[:role [::sm/one-of tt/valid-roles]]
[:email ::sm/email]])
(def ^:private check-create-invitation-params!

View file

@ -22,7 +22,7 @@
;; ----- Files
(defn sample-file
[label & {:keys [page-label name] :as params}]
[label & {:keys [page-label name view-only?] :as params}]
(binding [ffeat/*current* #{"components/v2"}]
(let [params (cond-> params
label
@ -35,7 +35,8 @@
(assoc :name "Test file"))
file (-> (ctf/make-file (dissoc params :page-label))
(assoc :features #{"components/v2"}))
(assoc :features #{"components/v2"})
(assoc :permissions {:can-edit (not (true? view-only?))}))
page (-> file
:data

View file

@ -178,19 +178,23 @@
(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)]
(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 (get-in state [:workspace-file :permissions :can-edit]))]
(rx/of (-> params
(assoc :undo-group undo-group)
(assoc :features features)
(assoc :tags tags)
(assoc :stack-undo? stack-undo?)
(assoc :save-undo? save-undo?)
(assoc :file-id file-id)
(assoc :file-revn (resolve-file-revn state file-id))
(assoc :undo-changes uchg)
(assoc :redo-changes rchg)
(commit)))))))
;; Prevent commit changes by a viewer team member (it really should never happen)
(if user-viewer?
(rx/empty)
(rx/of (-> params
(assoc :undo-group undo-group)
(assoc :features features)
(assoc :tags tags)
(assoc :stack-undo? stack-undo?)
(assoc :save-undo? save-undo?)
(assoc :file-id file-id)
(assoc :file-revn (resolve-file-revn state file-id))
(assoc :undo-changes uchg)
(assoc :redo-changes rchg)
(commit))))))))

View file

@ -7,7 +7,9 @@
(ns app.main.data.common
"A general purpose events."
(:require
[app.common.data.macros :as dm]
[app.common.types.components-list :as ctkl]
[app.common.types.team :as tt]
[app.config :as cf]
[app.main.data.modal :as modal]
[app.main.data.notifications :as ntf]
@ -171,25 +173,47 @@
(rx/tap on-success)
(rx/catch on-error))))))
(defn change-team-permissions
[team-id role]
[{:keys [team-id role workspace?]}]
(dm/assert! (uuid? team-id))
(dm/assert! (contains? tt/valid-roles role))
(ptk/reify ::change-team-permissions
ptk/WatchEvent
(watch [_ _ _]
(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"))]
(rx/of (ntf/info msg))))
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)
(let [route (if workspace?
[:workspace-file :permissions]
[:teams team-id :permissions])]
(update-in state route
(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 :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 :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)
(= role :owner)
(assoc permissions :can-edit true :is-admin true :is-owner true)
:else
permissions))))))
:else
permissions)))))))

View file

@ -20,12 +20,10 @@
[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]
@ -480,27 +478,6 @@
: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))
@ -1237,5 +1214,5 @@
[{:keys [type] :as msg}]
(case type
:notification (dc/handle-notification msg)
:team-permissions-change (handle-team-permissions-change msg)
:team-permissions-change (dc/change-team-permissions (assoc msg :workspace? false))
nil))

View file

@ -12,8 +12,10 @@
[app.common.schema :as sm]
[app.common.uuid :as uuid]
[app.main.data.changes :as dch]
[app.main.data.common :refer [handle-notification]]
[app.main.data.common :refer [handle-notification change-team-permissions]]
[app.main.data.websocket :as dws]
[app.main.data.workspace.edition :as dwe]
[app.main.data.workspace.layout :as dwly]
[app.main.data.workspace.libraries :as dwl]
[app.util.globals :refer [global]]
[app.util.mouse :as mse]
@ -92,17 +94,39 @@
(rx/concat stream (rx/of (dws/send endmsg)))))))
(defn- handle-change-team-permissions
[{:keys [role] :as msg}]
(ptk/reify ::handle-change-team-permissions
ptk/WatchEvent
(watch [_ _ _]
(let [viewer? (= :viewer role)]
(rx/concat
(->> (rx/of :interrupt
(dwe/clear-edition-mode))
;; Delay so anything that launched :interrupt can finish
(rx/delay 500))
(if viewer?
(rx/of (dwly/set-options-mode :design))
(rx/empty))
(rx/of (change-team-permissions msg)))))))
(defn- process-message
[{:keys [type] :as msg}]
(case type
:join-file (handle-presence msg)
:leave-file (handle-presence msg)
:presence (handle-presence msg)
:disconnect (handle-presence msg)
:pointer-update (handle-pointer-update msg)
:file-change (handle-file-change msg)
:library-change (handle-library-change msg)
:notification (handle-notification msg)
:join-file (handle-presence msg)
:leave-file (handle-presence msg)
:presence (handle-presence msg)
:disconnect (handle-presence msg)
:pointer-update (handle-pointer-update msg)
:file-change (handle-file-change msg)
:library-change (handle-library-change msg)
:notification (handle-notification msg)
:team-permissions-change (handle-change-team-permissions (assoc msg :workspace? true))
nil))
(defn- handle-pointer-send
@ -257,3 +281,7 @@
(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))))))

View file

@ -44,8 +44,12 @@
(defn emit-when-no-readonly
[& events]
(when-not (deref refs/workspace-read-only?)
(run! st/emit! events)))
(let [file (deref refs/workspace-file)
user-viewer? (not (get-in file [:permissions :can-edit]))
read-only? (or (deref refs/workspace-read-only?)
user-viewer?)]
(when-not read-only?
(run! st/emit! events))))
(def esc-pressed
(ptk/reify ::esc-pressed

View file

@ -189,7 +189,10 @@
(defn- update-attrs-when-no-readonly [props]
(let [undo-id (js/Symbol)
read-only? (deref refs/workspace-read-only?)
file (deref refs/workspace-file)
user-viewer? (not (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)

View file

@ -31,3 +31,5 @@
(def workspace-read-only? (mf/create-context nil))
(def is-component? (mf/create-context false))
(def sidebar (mf/create-context nil))
(def user-viewer? (mf/create-context nil))

View file

@ -164,7 +164,7 @@
(let [layout (mf/deref refs/workspace-layout)
wglobal (mf/deref refs/workspace-global)
read-only? (mf/deref refs/workspace-read-only?)
file (mf/deref refs/workspace-file)
project (mf/deref refs/workspace-project)
@ -172,6 +172,10 @@
team-id (:team-id project)
file-name (:name file)
user-viewer? (not (get-in file [:permissions :can-edit]))
read-only? (or (mf/deref refs/workspace-read-only?)
user-viewer?)
file-ready* (mf/with-memo [file-id]
(make-file-ready-ref file-id))
file-ready? (mf/deref file-ready*)
@ -210,13 +214,14 @@
[:& (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?}
[:section {:class (stl/css :workspace)
:style {:background-color background-color
:touch-action "none"}}
[:& context-menu]
(if ^boolean file-ready?
[:& workspace-page {:page-id page-id
:file file
:wglobal wglobal
:layout layout}]
[:& workspace-loader])]]]]]]]))
[:& (mf/provider ctx/user-viewer?) {:value user-viewer?}
[:section {:class (stl/css :workspace)
:style {:background-color background-color
:touch-action "none"}}
[:& context-menu]
(if ^boolean file-ready?
[:& workspace-page {:page-id page-id
:file file
:wglobal wglobal
:layout layout}]
[:& workspace-loader])]]]]]]]]))

View file

@ -466,13 +466,13 @@
(mf/defc file-menu
{::mf/wrap-props false}
[{:keys [on-close file]}]
(let [file-id (:id file)
shared? (:is-shared file)
[{:keys [on-close file user-viewer?]}]
(let [file-id (:id file)
shared? (:is-shared file)
objects (mf/deref refs/workspace-page-objects)
frames (->> (cfh/get-immediate-children objects uuid/zero)
(filterv cfh/frame-shape?))
objects (mf/deref refs/workspace-page-objects)
frames (->> (cfh/get-immediate-children objects uuid/zero)
(filterv cfh/frame-shape?))
on-remove-shared
(mf/use-fn
@ -565,11 +565,12 @@
:id "file-menu-remove-shared"}
[:span {:class (stl/css :item-name)} (tr "dashboard.unpublish-shared")]]
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click on-add-shared
:on-key-down on-add-shared-key-down
:id "file-menu-add-shared"}
[:span {:class (stl/css :item-name)} (tr "dashboard.add-shared")]])
(when-not user-viewer?
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click on-add-shared
:on-key-down on-add-shared-key-down
:id "file-menu-add-shared"}
[:span {:class (stl/css :item-name)} (tr "dashboard.add-shared")]]))
[:> dropdown-menu-item* {:class (stl/css :submenu-item)
:on-click on-export-shapes
@ -657,6 +658,8 @@
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]
@ -732,16 +735,17 @@
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.file")]
[:span {:class (stl/css :open-arrow)} i/arrow]]
[:> dropdown-menu-item* {:class (stl/css :menu-item)
:on-click on-menu-click
:on-key-down (fn [event]
(when (kbd/enter? event)
(on-menu-click event)))
:on-pointer-enter on-menu-click
:data-testid "edit"
:id "file-menu-edit"}
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.edit")]
[:span {:class (stl/css :open-arrow)} i/arrow]]
(when-not user-viewer?
[:> dropdown-menu-item* {:class (stl/css :menu-item)
:on-click on-menu-click
:on-key-down (fn [event]
(when (kbd/enter? event)
(on-menu-click event)))
:on-pointer-enter on-menu-click
:data-testid "edit"
:id "file-menu-edit"}
[:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.edit")]
[:span {:class (stl/css :open-arrow)} i/arrow]])
[:> dropdown-menu-item* {:class (stl/css :menu-item)
:on-click on-menu-click
@ -793,7 +797,8 @@
:file
[:& file-menu
{:file file
:on-close close-sub-menu}]
:on-close close-sub-menu
:user-viewer? user-viewer?}]
:edit
[:& edit-menu

View file

@ -184,8 +184,8 @@
on-click
(mf/use-fn
(mf/deps color-id apply-color on-asset-click)
(do
(mf/deps color-id apply-color on-asset-click read-only?)
(when-not read-only?
(dwl/add-recent-color color)
(partial on-asset-click color-id apply-color)))]

View file

@ -272,9 +272,10 @@
apply-typography
(mf/use-fn
(mf/deps file-id)
(mf/deps file-id read-only?)
(fn [typography _event]
(st/emit! (dwt/apply-typography typography file-id))))
(when-not read-only?
(st/emit! (dwt/apply-typography typography file-id)))))
create-group
(mf/use-fn

View file

@ -134,6 +134,8 @@
[{: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?)
selected-shapes (into [] (keep (d/getf objects)) selected)
first-selected-shape (first selected-shapes)
shape-parent-frame (cfh/get-frame objects (:frame-id first-selected-shape))
@ -173,17 +175,21 @@
tabs
#js [#js {:label (tr "workspace.options.design")
:id "design"
:content design-content}
(if user-viewer?
#js [#js {:label (tr "workspace.options.inspect")
:id "inspect"
:content inspect-content}]
#js [#js {:label (tr "workspace.options.design")
:id "design"
:content design-content}
#js {:label (tr "workspace.options.prototype")
:id "prototype"
:content interactions-content}
#js {:label (tr "workspace.options.prototype")
:id "prototype"
:content interactions-content}
#js {:label (tr "workspace.options.inspect")
:id "inspect"
:content inspect-content}]]
:content inspect-content}])]
[:div {:class (stl/css :tool-window)}
[:> tab-switcher* {:tabs tabs

View file

@ -208,6 +208,7 @@
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")}}
@ -221,8 +222,8 @@
(if ^boolean read-only?
(when (not ^boolean user-viewer?)
[:& badge-notification {:is-focus true
:size :small
:content (tr "labels.view-only")}])
:size :small
:content (tr "labels.view-only")}])
[:button {:class (stl/css :add-page)
:on-click on-create}
i/add])]

View file

@ -96,6 +96,7 @@
vbox' (mf/use-debounce 100 vbox)
;; DEREFS
user-viewer? (mf/use-ctx ctx/user-viewer?)
drawing (mf/deref refs/workspace-drawing)
focus (mf/deref refs/workspace-focus-selected)
@ -277,7 +278,8 @@
(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"}
[:& top-bar/top-bar {:layout layout}]
(when-not user-viewer?
[:& 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
;; inside a foreign object "dummy" so this awkward behaviour is take into account
@ -286,12 +288,13 @@
[: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}}
[:& stvh/viewport-texts
{:key (dm/str "texts-" page-id)
:page-id page-id
:objects objects
:modifiers modifiers
:edition edition}]]]]
(when-not workspace-read-only?
[:& stvh/viewport-texts
{:key (dm/str "texts-" page-id)
:page-id page-id
:objects objects
:modifiers modifiers
:edition edition}])]]]
(when show-comments?
[:& comments/comments-layer {:vbox vbox