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:
parent
cf150891df
commit
226ab7233b
17 changed files with 187 additions and 121 deletions
|
@ -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
|
||||
|
|
|
@ -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!
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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))))))))
|
||||
|
|
|
@ -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)))))))
|
|
@ -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))
|
|
@ -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))))))
|
||||
|
||||
|
||||
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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))
|
||||
|
|
|
@ -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])]]]]]]]]))
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)))]
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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])]
|
||||
|
|
|
@ -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
|
||||
|
|
Loading…
Add table
Reference in a new issue