From cf150891df42f9c0ed0725ecdf249dbc782129bd Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 26 Sep 2024 16:28:31 +0200 Subject: [PATCH 1/8] :sparkles: Add view mode to dashboard --- backend/src/app/rpc/commands/binfile.clj | 2 +- backend/src/app/rpc/commands/teams.clj | 29 ++--- common/src/app/common/types/team.cljc | 10 ++ .../assets/empty-placeholder-1-left.svg | 3 + .../assets/empty-placeholder-1-right.svg | 3 + .../assets/empty-placeholder-2-left.svg | 2 + .../assets/empty-placeholder-2-right.svg | 3 + frontend/src/app/main/data/common.cljs | 23 ++++ frontend/src/app/main/data/dashboard.cljs | 52 +++++++-- frontend/src/app/main/ui/dashboard.cljs | 16 ++- .../src/app/main/ui/dashboard/file_menu.cljs | 42 ++++--- frontend/src/app/main/ui/dashboard/files.cljs | 84 ++++++++------ frontend/src/app/main/ui/dashboard/files.scss | 4 + frontend/src/app/main/ui/dashboard/fonts.cljs | 83 ++++++++------ frontend/src/app/main/ui/dashboard/fonts.scss | 9 +- frontend/src/app/main/ui/dashboard/grid.cljs | 106 ++++++++++-------- .../src/app/main/ui/dashboard/libraries.cljs | 3 +- .../app/main/ui/dashboard/placeholder.cljs | 18 +-- .../app/main/ui/dashboard/placeholder.scss | 12 ++ .../app/main/ui/dashboard/project_menu.cljs | 8 +- .../src/app/main/ui/dashboard/projects.cljs | 95 ++++++++++------ .../src/app/main/ui/dashboard/projects.scss | 4 + frontend/src/app/main/ui/dashboard/team.cljs | 29 ++--- frontend/src/app/main/ui/ds.cljs | 2 + frontend/src/app/main/ui/ds/_sizes.scss | 3 + .../ui/ds/foundations/assets/raw_svg.cljs | 4 + .../app/main/ui/ds/notifications/toast.cljs | 2 - .../main/ui/ds/product/empty_placeholder.cljs | 40 +++++++ .../main/ui/ds/product/empty_placeholder.scss | 38 +++++++ .../ds/product/empty_placeholder.stories.jsx | 33 ++++++ .../app/main/ui/onboarding/team_choice.cljs | 8 +- .../main/ui/workspace/sidebar/options.cljs | 6 +- .../options/menus/color_selection.cljs | 2 - .../main/ui/workspace/sidebar/sitemap.cljs | 10 +- frontend/translations/en.po | 73 ++++++++++++ frontend/translations/es.po | 78 ++++++++++++- 36 files changed, 699 insertions(+), 240 deletions(-) create mode 100644 common/src/app/common/types/team.cljc create mode 100644 frontend/resources/images/assets/empty-placeholder-1-left.svg create mode 100644 frontend/resources/images/assets/empty-placeholder-1-right.svg create mode 100644 frontend/resources/images/assets/empty-placeholder-2-left.svg create mode 100644 frontend/resources/images/assets/empty-placeholder-2-right.svg create mode 100644 frontend/src/app/main/ui/ds/product/empty_placeholder.cljs create mode 100644 frontend/src/app/main/ui/ds/product/empty_placeholder.scss create mode 100644 frontend/src/app/main/ui/ds/product/empty_placeholder.stories.jsx diff --git a/backend/src/app/rpc/commands/binfile.clj b/backend/src/app/rpc/commands/binfile.clj index 6b2b69c90..144a1edda 100644 --- a/backend/src/app/rpc/commands/binfile.clj +++ b/backend/src/app/rpc/commands/binfile.clj @@ -89,7 +89,7 @@ ::sse/stream? true ::sm/params schema:import-binfile} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id name project-id file] :as params}] - (projects/check-read-permissions! pool profile-id project-id) + (projects/check-edition-permissions! pool profile-id project-id) (let [cfg (-> cfg (assoc ::bf.v1/project-id project-id) (assoc ::bf.v1/profile-id profile-id) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index e162e3358..62a426b02 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -12,6 +12,7 @@ [app.common.features :as cfeat] [app.common.logging :as l] [app.common.schema :as sm] + [app.common.types.team :as tt] [app.common.uuid :as uuid] [app.config :as cf] [app.db :as db] @@ -20,6 +21,7 @@ [app.loggers.audit :as audit] [app.main :as-alias main] [app.media :as media] + [app.msgbus :as mbus] [app.rpc :as-alias rpc] [app.rpc.commands.profile :as profile] [app.rpc.doc :as-alias doc] @@ -605,14 +607,8 @@ nil))) ;; --- Mutation: Team Update Role - -;; Temporarily disabled viewer role -;; https://tree.taiga.io/project/penpot/issue/1083 -(def valid-roles - #{:owner :admin :editor #_:viewer}) - (def schema:role - [::sm/one-of valid-roles]) + [::sm/one-of tt/valid-roles]) (defn role->params [role] @@ -623,7 +619,7 @@ :viewer {:is-owner false :is-admin false :can-edit false})) (defn update-team-member-role - [conn {:keys [profile-id team-id member-id role] :as params}] + [{:keys [::db/conn ::mbus/msgbus]} {:keys [profile-id team-id member-id role] :as params}] ;; We retrieve all team members instead of query the ;; database for a single member. This is just for ;; convenience, if this becomes a bottleneck or problematic, @@ -631,7 +627,6 @@ (let [perms (get-permissions conn profile-id team-id) members (get-team-members conn team-id) member (d/seek #(= member-id (:id %)) members) - is-owner? (:is-owner perms) is-admin? (:is-admin perms)] @@ -655,6 +650,13 @@ (ex/raise :type :validation :code :cant-promote-to-owner)) + (mbus/pub! msgbus + :topic member-id + :message {:type :team-permissions-change + :subs-id member-id + :team-id team-id + :role role}) + (let [params (role->params role)] ;; Only allow single owner on team (when (= role :owner) @@ -678,9 +680,8 @@ (sv/defmethod ::update-team-member-role {::doc/added "1.17" ::sm/params schema:update-team-member-role} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id] :as params}] - (db/with-atomic [conn pool] - (update-team-member-role conn (assoc params :profile-id profile-id)))) + [cfg {:keys [::rpc/profile-id] :as params}] + (db/tx-run! cfg update-team-member-role (assoc params :profile-id profile-id))) ;; --- Mutation: Delete Team Member @@ -724,6 +725,7 @@ ::sm/params schema:update-team-photo} [cfg {:keys [::rpc/profile-id file] :as params}] ;; Validate incoming mime type + (media/validate-media-type! file #{"image/jpeg" "image/png" "image/webp"}) (update-team-photo cfg (assoc params :profile-id profile-id))) @@ -1115,7 +1117,7 @@ ::sm/params schema:update-team-invitation-role} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id email role] :as params}] (db/with-atomic [conn pool] - (let [perms (get-permissions conn profile-id team-id)] + (let [perms (get-permissions conn profile-id team-id)] (when-not (:is-admin perms) (ex/raise :type :validation @@ -1124,6 +1126,7 @@ (db/update! conn :team-invitation {:role (name role) :updated-at (dt/now)} {:team-id team-id :email-to (profile/clean-email email)}) + nil))) ;; --- Mutation: Delete invitation diff --git a/common/src/app/common/types/team.cljc b/common/src/app/common/types/team.cljc new file mode 100644 index 000000000..5eaa3787d --- /dev/null +++ b/common/src/app/common/types/team.cljc @@ -0,0 +1,10 @@ +;; 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.common.types.team) + +(def valid-roles + #{:owner :admin :editor :viewer}) \ No newline at end of file diff --git a/frontend/resources/images/assets/empty-placeholder-1-left.svg b/frontend/resources/images/assets/empty-placeholder-1-left.svg new file mode 100644 index 000000000..dc40e1fbf --- /dev/null +++ b/frontend/resources/images/assets/empty-placeholder-1-left.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/resources/images/assets/empty-placeholder-1-right.svg b/frontend/resources/images/assets/empty-placeholder-1-right.svg new file mode 100644 index 000000000..102b75d2c --- /dev/null +++ b/frontend/resources/images/assets/empty-placeholder-1-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/resources/images/assets/empty-placeholder-2-left.svg b/frontend/resources/images/assets/empty-placeholder-2-left.svg new file mode 100644 index 000000000..7ccd30fe4 --- /dev/null +++ b/frontend/resources/images/assets/empty-placeholder-2-left.svg @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/frontend/resources/images/assets/empty-placeholder-2-right.svg b/frontend/resources/images/assets/empty-placeholder-2-right.svg new file mode 100644 index 000000000..25c48b142 --- /dev/null +++ b/frontend/resources/images/assets/empty-placeholder-2-right.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index a9b219f78..3a00faff9 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -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)))))) \ No newline at end of file diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 1bedc29dd..98ed715f5 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -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)) \ No newline at end of file diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index a15822104..eff810496 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -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}] diff --git a/frontend/src/app/main/ui/dashboard/file_menu.cljs b/frontend/src/app/main/ui/dashboard/file_menu.cljs index 8d6e01f7b..aa87cb907 100644 --- a/frontend/src/app/main/ui/dashboard/file_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/file_menu.cljs @@ -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" diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index e533a6b85..13ea519c6 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -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}])]])) diff --git a/frontend/src/app/main/ui/dashboard/files.scss b/frontend/src/app/main/ui/dashboard/files.scss index 7c37cd57c..692eed37c 100644 --- a/frontend/src/app/main/ui/dashboard/files.scss +++ b/frontend/src/app/main/ui/dashboard/files.scss @@ -35,3 +35,7 @@ @extend .button-icon; stroke: var(--icon-foreground); } + +.placeholder-placement { + margin: $s-16 $s-32; +} diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index 519599243..c6c5d59af 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -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}] diff --git a/frontend/src/app/main/ui/dashboard/fonts.scss b/frontend/src/app/main/ui/dashboard/fonts.scss index fd40fc50d..4bfea724e 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.scss +++ b/frontend/src/app/main/ui/dashboard/fonts.scss @@ -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; } } diff --git a/frontend/src/app/main/ui/dashboard/grid.cljs b/frontend/src/app/main/ui/dashboard/grid.cljs index 15245d39c..9ad507f68 100644 --- a/frontend/src/app/main/ui/dashboard/grid.cljs +++ b/frontend/src/app/main/ui/dashboard/grid.cljs @@ -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}])])) diff --git a/frontend/src/app/main/ui/dashboard/libraries.cljs b/frontend/src/app/main/ui/dashboard/libraries.cljs index 78238721e..2ef394fa8 100644 --- a/frontend/src/app/main/ui/dashboard/libraries.cljs +++ b/frontend/src/app/main/ui/dashboard/libraries.cljs @@ -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}]]])) diff --git a/frontend/src/app/main/ui/dashboard/placeholder.cljs b/frontend/src/app/main/ui/dashboard/placeholder.cljs index 261fe3c4f..00162f1c0 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.cljs +++ b/frontend/src/app/main/ui/dashboard/placeholder.cljs @@ -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]]))) diff --git a/frontend/src/app/main/ui/dashboard/placeholder.scss b/frontend/src/app/main/ui/dashboard/placeholder.scss index a72ebc451..da06dd863 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.scss +++ b/frontend/src/app/main/ui/dashboard/placeholder.scss @@ -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; +} diff --git a/frontend/src/app/main/ui/dashboard/project_menu.cljs b/frontend/src/app/main/ui/dashboard/project_menu.cljs index a8eb4621d..36293395d 100644 --- a/frontend/src/app/main/ui/dashboard/project_menu.cljs +++ b/frontend/src/app/main/ui/dashboard/project_menu.cljs @@ -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}]])) diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index 46e8828e0..f2f5c20ee 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -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}]))]]]]))) diff --git a/frontend/src/app/main/ui/dashboard/projects.scss b/frontend/src/app/main/ui/dashboard/projects.scss index e544896ee..a40fce6a3 100644 --- a/frontend/src/app/main/ui/dashboard/projects.scss +++ b/frontend/src/app/main/ui/dashboard/projects.scss @@ -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; diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 3770fb568..ace323cd1 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -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} diff --git a/frontend/src/app/main/ui/ds.cljs b/frontend/src/app/main/ui/ds.cljs index 84a70e0e3..89f8a4961 100644 --- a/frontend/src/app/main/ui/ds.cljs +++ b/frontend/src/app/main/ui/ds.cljs @@ -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* diff --git a/frontend/src/app/main/ui/ds/_sizes.scss b/frontend/src/app/main/ui/ds/_sizes.scss index 63ad1f93b..1011d1285 100644 --- a/frontend/src/app/main/ui/ds/_sizes.scss +++ b/frontend/src/app/main/ui/ds/_sizes.scss @@ -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); diff --git a/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs b/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs index 2011cf4fa..c74188edb 100644 --- a/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs +++ b/frontend/src/app/main/ui/ds/foundations/assets/raw_svg.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/ds/notifications/toast.cljs b/frontend/src/app/main/ui/ds/notifications/toast.cljs index 82f399f2f..29968c09d 100644 --- a/frontend/src/app/main/ui/ds/notifications/toast.cljs +++ b/frontend/src/app/main/ui/ds/notifications/toast.cljs @@ -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 diff --git a/frontend/src/app/main/ui/ds/product/empty_placeholder.cljs b/frontend/src/app/main/ui/ds/product/empty_placeholder.cljs new file mode 100644 index 000000000..cbbeb4173 --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/empty_placeholder.cljs @@ -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)}]])) \ No newline at end of file diff --git a/frontend/src/app/main/ui/ds/product/empty_placeholder.scss b/frontend/src/app/main/ui/ds/product/empty_placeholder.scss new file mode 100644 index 000000000..cc4654b9e --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/empty_placeholder.scss @@ -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); +} diff --git a/frontend/src/app/main/ui/ds/product/empty_placeholder.stories.jsx b/frontend/src/app/main/ui/ds/product/empty_placeholder.stories.jsx new file mode 100644 index 000000000..6e86a1636 --- /dev/null +++ b/frontend/src/app/main/ui/ds/product/empty_placeholder.stories.jsx @@ -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 }) => , +}; + +export const Default = {}; + +export const AlternativeDecoration = { + args: { + type: 2, + }, +}; diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index 50ee2fba8..f54d4d48e 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -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) diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index abd2aaf7e..91edbe986 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs index e6c904beb..a24c3053d 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options/menus/color_selection.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index 09a02d2f9..f8ca6f90c 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -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])] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index b0b931a05..ff10bf4fa 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -439,6 +439,55 @@ msgstr "" "Files added to Libraries will appear here. Try sharing your files or add " "from our [Libraries & templates](https://penpot.app/libraries-templates)." + + + + + + + +#: src/app/main/ui/dashboard/placeholder.cljs +msgid "dashboard.empty-placeholder-libraries-title" +msgstr "No libraries yet." + +#: src/app/main/ui/dashboard/placeholder.cljs +#, markdown +msgid "dashboard.empty-placeholder-libraries-subtitle" +msgstr "" +"Files added to Libraries will appear here. Try sharing your files or add " +"from our [Libraries & templates](https://penpot.app/libraries-templates)." + +#: src/app/main/ui/dashboard/placeholder.cljs +msgid "dashboard.empty-placeholder-libraries-subtitle-viewer-role" +msgstr "Files added to Libraries will appear here." + +#: src/app/main/ui/dashboard +msgid "dashboard.empty-placeholder-drafts-title" +msgstr "No drafts yet." + +#: src/app/main/ui/dashboard +msgid "dashboard.empty-placeholder-drafts-subtitle" +msgstr "Once a project member creates a draft, it will be displayed here." + +#: src/app/main/ui/dashboard +msgid "dashboard.empty-placeholder-files-title" +msgstr "No files yet." + +#: src/app/main/ui/dashboard +msgid "dashboard.empty-placeholder-files-subtitle" +msgstr "Once a project member creates a file, it will be displayed here." + + + + + + + + + + + + #: src/app/main/ui/dashboard/file_menu.cljs:249 msgid "dashboard.export-binary-multi" msgstr "Download %s Penpot files (.penpot)" @@ -541,6 +590,14 @@ msgstr "Dismiss all" msgid "dashboard.fonts.empty-placeholder" msgstr "Custom fonts you upload will appear here." +#: src/app/main/ui/dashboard/fonts.cljs:436 +msgid "dashboard.fonts.empty-placeholder-viewer" +msgstr "No custom fonts yet." + +#: src/app/main/ui/dashboard/fonts.cljs:436 +msgid "dashboard.fonts.empty-placeholder-viewer-sub" +msgstr "Once a project member uploads a custom font, it will be displayed here." + #: src/app/main/ui/dashboard/fonts.cljs:194 msgid "dashboard.fonts.fonts-added" msgid_plural "dashboard.fonts.fonts-added" @@ -695,6 +752,22 @@ msgstr "+ New project" msgid "dashboard.new-project-prefix" msgstr "New Project" +#: src/app/main/data/dashboard.cljs:72 +msgid "dashboard.permissions-change.viewer" +msgstr "You are now a viewer on this team." + +#: src/app/main/data/dashboard.cljs:75 +msgid "dashboard.permissions-change.editor" +msgstr "You are now an editor on this team." + +#: src/app/main/data/dashboard.cljs:78 +msgid "dashboard.permissions-change.admin" +msgstr "You are now an admin on this team." + +#: src/app/main/data/dashboard.cljs:81 +msgid "dashboard.permissions-change.owner" +msgstr "You are now owner on this team." + #: src/app/main/ui/dashboard/search.cljs:60 msgid "dashboard.no-matches-for" msgstr "No matches found for “%s“" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 7400fd0b9..ec6795ed7 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -442,6 +442,58 @@ msgstr "" "con alguna plantilla ve a [Bibliotecas y " "plantillas](https://penpot.app/libraries-templates)." +#: src/app/main/ui/dashboard/placeholder.cljs:33 +msgid "dashboard.empty-placeholder-drafts-viewer-role" +msgstr "" +"Los archivos agregados a las bibliotecas aparecerán aquí." + + + + +#: src/app/main/ui/dashboard/placeholder.cljs +msgid "dashboard.empty-placeholder-libraries-title" +msgstr "Aún no existen librerías compartidas." + +#: src/app/main/ui/dashboard/placeholder.cljs +#, markdown +msgid "dashboard.empty-placeholder-libraries-subtitle" +msgstr "" +"Los archivos agregados a las bibliotecas aparecerán aquí. Si quieres probar " +"con alguna plantilla ve a [Bibliotecas y " +"plantillas](https://penpot.app/libraries-templates)." + +#: src/app/main/ui/dashboard/placeholder.cljs +msgid "dashboard.empty-placeholder-libraries-subtitle-viewer-role" +msgstr "Los archivos agregados a las bibliotecas aparecerán aquí." + + +#: src/app/main/ui/dashboard/files.cljs +msgid "dashboard.empty-placeholder-drafts-title" +msgstr "Aún no hay borradores." + +#: src/app/main/ui/dashboard/files.cljs +msgid "dashboard.empty-placeholder-drafts-subtitle" +msgstr "Cuando un miembro del equipo cree algún borrador, este aparecerá aquí." + +#: src/app/main/ui/dashboard/files.cljs +msgid "dashboard.empty-placeholder-files-title" +msgstr "Aún no hay archivos." + +#: src/app/main/ui/dashboard/files.cljs +msgid "dashboard.empty-placeholder-files-subtitle" +msgstr "Cuando un miembro del equipo cree algún archivo, este aparecerá aquí." + + + + + + + + + + + + #: src/app/main/ui/dashboard/file_menu.cljs:249 msgid "dashboard.export-binary-multi" msgstr "Descargar %s archivos Penpot (.penpot)" @@ -544,6 +596,14 @@ msgstr "Ignorar todas" msgid "dashboard.fonts.empty-placeholder" msgstr "Las fuentes personalizadas que subas aparecerán aquí." +#: src/app/main/ui/dashboard/fonts.cljs:436 +msgid "dashboard.fonts.empty-placeholder-viewer" +msgstr "Aún no hay fuentes personalizadas." + +#: src/app/main/ui/dashboard/fonts.cljs:436 +msgid "dashboard.fonts.empty-placeholder-viewer-sub" +msgstr "Cuando un miembro del equipo suba una fuente personalizada, esta aparecerá aquí." + #: src/app/main/ui/dashboard/fonts.cljs:194 msgid "dashboard.fonts.fonts-added" msgid_plural "dashboard.fonts.fonts-added" @@ -702,6 +762,22 @@ msgstr "+ Nuevo proyecto" msgid "dashboard.new-project-prefix" msgstr "Nuevo Proyecto" +#: src/app/main/data/dashboard.cljs:72 +msgid "dashboard.permissions-change.viewer" +msgstr "Ahora eres lector del equipo." + +#: src/app/main/data/dashboard.cljs:75 +msgid "dashboard.permissions-change.editor" +msgstr "Ahora eres editor del equipo." + +#: src/app/main/data/dashboard.cljs:78 +msgid "dashboard.permissions-change.admin" +msgstr "Ahora eres administrador del equipo." + +#: src/app/main/data/dashboard.cljs:81 +msgid "dashboard.permissions-change.owner" +msgstr "Ahora eres el dueño del equipo." + #: src/app/main/ui/dashboard/search.cljs:60 msgid "dashboard.no-matches-for" msgstr "No se encuentra “%s“" @@ -2025,7 +2101,7 @@ msgstr "Solo lectura" #: src/app/main/ui/dashboard/team.cljs:128, src/app/main/ui/dashboard/team.cljs:301, src/app/main/ui/dashboard/team.cljs:540 msgid "labels.viewer" -msgstr "Visualizador" +msgstr "Lector" #: src/app/main/ui/dashboard/sidebar.cljs:523, src/app/main/ui/dashboard/team.cljs:95, src/app/main/ui/dashboard/team.cljs:105, src/app/main/ui/dashboard/team.cljs:901 msgid "labels.webhooks" From 226ab7233b06ccd8e8188771d3a0468ac032101d Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Fri, 27 Sep 2024 17:02:39 +0200 Subject: [PATCH 2/8] :sparkles: Add viewer role to workspace --- CHANGES.md | 2 + backend/src/app/rpc/commands/teams.clj | 2 +- common/src/app/common/test_helpers/files.cljc | 5 +- frontend/src/app/main/data/changes.cljs | 34 ++++++------ frontend/src/app/main/data/common.cljs | 52 ++++++++++++++----- frontend/src/app/main/data/dashboard.cljs | 25 +-------- .../main/data/workspace/notifications.cljs | 46 ++++++++++++---- .../app/main/data/workspace/shortcuts.cljs | 8 ++- .../main/data/workspace/text/shortcuts.cljs | 5 +- frontend/src/app/main/ui/context.cljs | 2 + frontend/src/app/main/ui/workspace.cljs | 27 ++++++---- .../src/app/main/ui/workspace/main_menu.cljs | 49 +++++++++-------- .../ui/workspace/sidebar/assets/colors.cljs | 4 +- .../sidebar/assets/typographies.cljs | 5 +- .../main/ui/workspace/sidebar/options.cljs | 20 ++++--- .../main/ui/workspace/sidebar/sitemap.cljs | 5 +- .../src/app/main/ui/workspace/viewport.cljs | 17 +++--- 17 files changed, 187 insertions(+), 121 deletions(-) diff --git a/CHANGES.md b/CHANGES.md index b2d836bb8..548b9c0ab 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 62a426b02..b41279c7e 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -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! diff --git a/common/src/app/common/test_helpers/files.cljc b/common/src/app/common/test_helpers/files.cljc index 59b166555..7d384b0ef 100644 --- a/common/src/app/common/test_helpers/files.cljc +++ b/common/src/app/common/test_helpers/files.cljc @@ -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 diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index 080f193fa..cfc6a41fa 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -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)))))))) diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index 3a00faff9..7cdc00f20 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -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)))))) \ No newline at end of file + :else + permissions))))))) \ No newline at end of file diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 98ed715f5..88614547b 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -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)) \ No newline at end of file diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index e602618e1..1b5a01203 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -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)))))) + + + + diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index fd045067a..444ed499e 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -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 diff --git a/frontend/src/app/main/data/workspace/text/shortcuts.cljs b/frontend/src/app/main/data/workspace/text/shortcuts.cljs index 0970fca8b..d67b327d3 100644 --- a/frontend/src/app/main/data/workspace/text/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/text/shortcuts.cljs @@ -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) diff --git a/frontend/src/app/main/ui/context.cljs b/frontend/src/app/main/ui/context.cljs index c007896b9..9323171ce 100644 --- a/frontend/src/app/main/ui/context.cljs +++ b/frontend/src/app/main/ui/context.cljs @@ -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)) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index a284ec28e..75d422137 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -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])]]]]]]]])) diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index fb2ba7148..ec2516362 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs index 7f7e67d92..06e12e3f0 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/colors.cljs @@ -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)))] diff --git a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs index 022bbebc9..5b8f2a3a8 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/assets/typographies.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index 91edbe986..e10fa59a5 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -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 diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index f8ca6f90c..f73417803 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -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])] diff --git a/frontend/src/app/main/ui/workspace/viewport.cljs b/frontend/src/app/main/ui/workspace/viewport.cljs index 7040c2c1e..98d4d2f9b 100644 --- a/frontend/src/app/main/ui/workspace/viewport.cljs +++ b/frontend/src/app/main/ui/workspace/viewport.cljs @@ -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 From 823792339f2ddda9b119e464b4151d6eca9905e1 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Mon, 14 Oct 2024 15:19:40 +0200 Subject: [PATCH 3/8] :sparkles: Kick out of a team - Visibility of System Status --- backend/src/app/rpc/commands/teams.clj | 12 ++++++++-- frontend/src/app/main/data/common.cljs | 22 ++++++++++++++++++- frontend/src/app/main/data/dashboard.cljs | 1 + frontend/src/app/main/data/workspace.cljs | 17 -------------- .../src/app/main/data/workspace/common.cljs | 21 ++++++++++++++++++ .../main/data/workspace/notifications.cljs | 22 ++++++++++--------- .../main/ui/workspace/sidebar/options.cljs | 5 +++-- .../main/ui/workspace/viewport/top_bar.cljs | 3 ++- frontend/src/debug.cljs | 3 ++- frontend/translations/en.po | 12 ++++++---- frontend/translations/es.po | 12 ++++++---- 11 files changed, 88 insertions(+), 42 deletions(-) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index b41279c7e..54f523404 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -693,9 +693,10 @@ (sv/defmethod ::delete-team-member {::doc/added "1.17" ::sm/params schema:delete-team-member} - [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}] + [{:keys [::db/pool ::mbus/msgbus] :as cfg} {:keys [::rpc/profile-id team-id member-id] :as params}] (db/with-atomic [conn pool] - (let [perms (get-permissions conn profile-id team-id)] + (let [team (get-team pool :profile-id profile-id :team-id team-id) + perms (get-permissions conn profile-id team-id)] (when-not (or (:is-owner perms) (:is-admin perms)) (ex/raise :type :validation @@ -708,6 +709,13 @@ (db/delete! conn :team-profile-rel {:profile-id member-id :team-id team-id}) + (mbus/pub! msgbus + :topic member-id + :message {:type :removed-from-team + :subs-id member-id + :team-id team-id + :team-name (:name team)}) + nil))) ;; --- Mutation: Update Team Photo diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index 7cdc00f20..f59707028 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -17,6 +17,7 @@ [app.main.repo :as rp] [app.main.store :as st] [app.util.i18n :refer [tr]] + [app.util.router :as rt] [beicon.v2.core :as rx] [potok.v2.core :as ptk])) @@ -216,4 +217,23 @@ (assoc permissions :can-edit true :is-admin true :is-owner true) :else - permissions))))))) \ No newline at end of file + permissions))))))) + + + +(defn removed-from-team + [{:keys [team-id team-name]}] + (dm/assert! (uuid? team-id)) + (ptk/reify ::removed-from-team + ptk/WatchEvent + (watch [_ state _] + (let [msg (tr "dashboard.removed-from-team" team-name)] + + (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/delay 250))))))) + + + diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 88614547b..ead095adb 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -1215,4 +1215,5 @@ (case type :notification (dc/handle-notification msg) :team-permissions-change (dc/change-team-permissions (assoc msg :workspace? false)) + :removed-from-team (dc/removed-from-team msg) nil)) \ No newline at end of file diff --git a/frontend/src/app/main/data/workspace.cljs b/frontend/src/app/main/data/workspace.cljs index 5afe5eeeb..78c0f8615 100644 --- a/frontend/src/app/main/data/workspace.cljs +++ b/frontend/src/app/main/data/workspace.cljs @@ -2106,24 +2106,7 @@ (pcb/mod-page {:background (:color color)}))] (rx/of (dch/commit-changes changes))))))) -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -;; Read only -;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; -(defn set-workspace-read-only - [read-only?] - (ptk/reify ::set-workspace-read-only - ptk/UpdateEvent - (update [_ state] - (assoc-in state [:workspace-global :read-only?] read-only?)) - - ptk/WatchEvent - (watch [_ _ _] - (if read-only? - (rx/of :interrupt - (remove-layout-flag :colorpalette) - (remove-layout-flag :textpalette)) - (rx/empty))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Measurements diff --git a/frontend/src/app/main/data/workspace/common.cljs b/frontend/src/app/main/data/workspace/common.cljs index 587995105..3902252c5 100644 --- a/frontend/src/app/main/data/workspace/common.cljs +++ b/frontend/src/app/main/data/workspace/common.cljs @@ -7,6 +7,8 @@ (ns app.main.data.workspace.common (:require [app.common.logging :as log] + [app.main.data.workspace.layout :as dwl] + [beicon.v2.core :as rx] [potok.v2.core :as ptk])) ;; Change this to :info :debug or :trace to debug this module @@ -56,3 +58,22 @@ ptk/UpdateEvent (update [_ state] (update-in state [:workspace-local :hide-toolbar] not)))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Read only +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn set-workspace-read-only + [read-only?] + (ptk/reify ::set-workspace-read-only + ptk/UpdateEvent + (update [_ state] + (assoc-in state [:workspace-global :read-only?] read-only?)) + + ptk/WatchEvent + (watch [_ _ _] + (if read-only? + (rx/of :interrupt + (dwl/remove-layout-flag :colorpalette) + (dwl/remove-layout-flag :textpalette)) + (rx/empty))))) diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 1b5a01203..85093bc74 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -12,10 +12,12 @@ [app.common.schema :as sm] [app.common.uuid :as uuid] [app.main.data.changes :as dch] - [app.main.data.common :refer [handle-notification change-team-permissions]] + [app.main.data.common :as dc] [app.main.data.websocket :as dws] + [app.main.data.workspace.common :as dwc] [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] @@ -103,16 +105,15 @@ (let [viewer? (= :viewer role)] (rx/concat - (->> (rx/of :interrupt - (dwe/clear-edition-mode)) + (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 500)) - + (rx/delay 100)) (if viewer? - (rx/of (dwly/set-options-mode :design)) - (rx/empty)) - - (rx/of (change-team-permissions msg))))))) + (rx/of (dwly/set-options-mode :inspect)) + (rx/of (dwly/set-options-mode :design)))))))) (defn- process-message @@ -125,8 +126,9 @@ :pointer-update (handle-pointer-update msg) :file-change (handle-file-change msg) :library-change (handle-library-change msg) - :notification (handle-notification msg) + :notification (dc/handle-notification msg) :team-permissions-change (handle-change-team-permissions (assoc msg :workspace? true)) + :removed-from-team (dc/removed-from-team msg) nil)) (defn- handle-pointer-send diff --git a/frontend/src/app/main/ui/workspace/sidebar/options.cljs b/frontend/src/app/main/ui/workspace/sidebar/options.cljs index e10fa59a5..a7505dde3 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/options.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/options.cljs @@ -13,6 +13,7 @@ [app.common.geom.shapes :as gsh] [app.common.types.shape.layout :as ctl] [app.main.data.workspace :as udw] + [app.main.data.workspace.common :as dwc] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] @@ -147,8 +148,8 @@ (let [options-mode (keyword options-mode)] (st/emit! (udw/set-options-mode options-mode)) (if (= options-mode :inspect) - (st/emit! :interrupt (udw/set-workspace-read-only true)) - (st/emit! :interrupt (udw/set-workspace-read-only false))))) + (st/emit! :interrupt (dwc/set-workspace-read-only true)) + (st/emit! :interrupt (dwc/set-workspace-read-only false))))) design-content (mf/html [:& design-menu {:selected selected diff --git a/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs b/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs index 7c2963ae1..6767735db 100644 --- a/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/top_bar.cljs @@ -10,6 +10,7 @@ [app.common.files.helpers :as cfh] [app.common.types.shape.layout :as ctl] [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwc] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] @@ -26,7 +27,7 @@ (fn [] (st/emit! :interrupt (dw/set-options-mode :design) - (dw/set-workspace-read-only false))))] + (dwc/set-workspace-read-only false))))] [:div {:class (stl/css :viewport-actions)} [:div {:class (stl/css :viewport-actions-container)} [:div {:class (stl/css :viewport-actions-title)} diff --git a/frontend/src/debug.cljs b/frontend/src/debug.cljs index d127c4bf1..d758c09d4 100644 --- a/frontend/src/debug.cljs +++ b/frontend/src/debug.cljs @@ -23,6 +23,7 @@ [app.main.data.preview :as dp] [app.main.data.viewer.shortcuts] [app.main.data.workspace :as dw] + [app.main.data.workspace.common :as dwcm] [app.main.data.workspace.path.shortcuts] [app.main.data.workspace.selection :as dws] [app.main.data.workspace.shortcuts] @@ -370,7 +371,7 @@ (defn ^:export set-workspace-read-only [read-only?] - (st/emit! (dw/set-workspace-read-only read-only?))) + (st/emit! (dwcm/set-workspace-read-only read-only?))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; REPAIR & VALIDATION diff --git a/frontend/translations/en.po b/frontend/translations/en.po index ff10bf4fa..53979c6b1 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -752,22 +752,26 @@ msgstr "+ New project" msgid "dashboard.new-project-prefix" msgstr "New Project" -#: src/app/main/data/dashboard.cljs:72 +#: src/app/main/data/common.cljs:72 msgid "dashboard.permissions-change.viewer" msgstr "You are now a viewer on this team." -#: src/app/main/data/dashboard.cljs:75 +#: src/app/main/data/common.cljs:75 msgid "dashboard.permissions-change.editor" msgstr "You are now an editor on this team." -#: src/app/main/data/dashboard.cljs:78 +#: src/app/main/data/common.cljs:78 msgid "dashboard.permissions-change.admin" msgstr "You are now an admin on this team." -#: src/app/main/data/dashboard.cljs:81 +#: src/app/main/data/common.cljs:195 msgid "dashboard.permissions-change.owner" msgstr "You are now owner on this team." +#: src/app/main/data/common.cljs:229 +msgid "dashboard.removed-from-team" +msgstr "You are not part of the team “%s“ anymore." + #: src/app/main/ui/dashboard/search.cljs:60 msgid "dashboard.no-matches-for" msgstr "No matches found for “%s“" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index ec6795ed7..53ca42684 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -762,22 +762,26 @@ msgstr "+ Nuevo proyecto" msgid "dashboard.new-project-prefix" msgstr "Nuevo Proyecto" -#: src/app/main/data/dashboard.cljs:72 +#: src/app/main/data/common.cljs:72 msgid "dashboard.permissions-change.viewer" msgstr "Ahora eres lector del equipo." -#: src/app/main/data/dashboard.cljs:75 +#: src/app/main/data/common.cljs:75 msgid "dashboard.permissions-change.editor" msgstr "Ahora eres editor del equipo." -#: src/app/main/data/dashboard.cljs:78 +#: src/app/main/data/common.cljs:78 msgid "dashboard.permissions-change.admin" msgstr "Ahora eres administrador del equipo." -#: src/app/main/data/dashboard.cljs:81 +#: src/app/main/data/common.cljs:81 msgid "dashboard.permissions-change.owner" msgstr "Ahora eres el dueño del equipo." +#: src/app/main/data/common.cljs:229 +msgid "dashboard.removed-from-team" +msgstr "Ya no eres parte del equipo “%s“." + #: src/app/main/ui/dashboard/search.cljs:60 msgid "dashboard.no-matches-for" msgstr "No se encuentra “%s“" From 043c4105dbcca54986a1f7cc055bfd413e2aad7a Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Thu, 10 Oct 2024 14:02:05 +0200 Subject: [PATCH 4/8] :sparkles: Add viewer only mode on webhook --- backend/src/app/migrations.clj | 5 +- .../migrations/sql/0131-mod-webhook-table.sql | 6 + backend/src/app/rpc/commands/webhooks.clj | 32 +++- backend/test/backend_tests/helpers.clj | 1 + .../test/backend_tests/rpc_webhooks_test.clj | 156 ++++++++++++++++-- frontend/src/app/main/ui/dashboard/team.cljs | 45 +++-- frontend/src/app/main/ui/dashboard/team.scss | 10 +- .../app/main/ui/onboarding/team_choice.cljs | 2 +- .../main/ui/workspace/sidebar/sitemap.cljs | 4 +- frontend/translations/en.po | 4 + frontend/translations/es.po | 4 + 11 files changed, 223 insertions(+), 46 deletions(-) create mode 100644 backend/src/app/migrations/sql/0131-mod-webhook-table.sql diff --git a/backend/src/app/migrations.clj b/backend/src/app/migrations.clj index 5226e5152..0f24a6d77 100644 --- a/backend/src/app/migrations.clj +++ b/backend/src/app/migrations.clj @@ -412,7 +412,10 @@ :fn (mg/resource "app/migrations/sql/0129-mod-file-change-table.sql")} {:name "0130-mod-file-change-table" - :fn (mg/resource "app/migrations/sql/0130-mod-file-change-table.sql")}]) + :fn (mg/resource "app/migrations/sql/0130-mod-file-change-table.sql")} + + {:name "0131-mod-webhook-table" + :fn (mg/resource "app/migrations/sql/0131-mod-webhook-table.sql")}]) (defn apply-migrations! [pool name migrations] diff --git a/backend/src/app/migrations/sql/0131-mod-webhook-table.sql b/backend/src/app/migrations/sql/0131-mod-webhook-table.sql new file mode 100644 index 000000000..67995d60d --- /dev/null +++ b/backend/src/app/migrations/sql/0131-mod-webhook-table.sql @@ -0,0 +1,6 @@ +ALTER TABLE webhook + ADD COLUMN profile_id uuid NULL REFERENCES profile (id) ON DELETE SET NULL; + +CREATE INDEX webhook__profile_id__idx + ON webhook (profile_id) + WHERE profile_id IS NOT NULL; \ No newline at end of file diff --git a/backend/src/app/rpc/commands/webhooks.clj b/backend/src/app/rpc/commands/webhooks.clj index e2a56691e..1eb999c3e 100644 --- a/backend/src/app/rpc/commands/webhooks.clj +++ b/backend/src/app/rpc/commands/webhooks.clj @@ -15,12 +15,27 @@ [app.http.client :as http] [app.loggers.webhooks :as webhooks] [app.rpc :as-alias rpc] - [app.rpc.commands.teams :refer [check-edition-permissions! check-read-permissions!]] + [app.rpc.commands.teams :refer [check-read-permissions!] :as t] [app.rpc.doc :as-alias doc] + [app.rpc.permissions :as perms] [app.util.services :as sv] [app.util.time :as dt] [cuerdas.core :as str])) +(defn get-webhooks-permissions + [conn profile-id team-id creator-id] + (let [permissions (t/get-permissions conn profile-id team-id) + + can-edit (boolean (or (:can-edit permissions) + (= profile-id creator-id)))] + (assoc permissions :can-edit can-edit))) + +(def has-webhook-edit-permissions? + (perms/make-edition-predicate-fn get-webhooks-permissions)) + +(def check-webhook-edition-permissions! + (perms/make-check-fn has-webhook-edit-permissions?)) + (defn decode-row [{:keys [uri] :as row}] (cond-> row @@ -65,11 +80,12 @@ max-hooks-for-team))))) (defn- insert-webhook! - [{:keys [::db/pool]} {:keys [team-id uri mtype is-active] :as params}] + [{:keys [::db/pool]} {:keys [team-id uri mtype is-active ::rpc/profile-id] :as params}] (-> (db/insert! pool :webhook {:id (uuid/next) :team-id team-id :uri (str uri) + :profile-id profile-id :is-active is-active :mtype mtype}) (decode-row))) @@ -101,7 +117,7 @@ {::doc/added "1.17" ::sm/params schema:create-webhook} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id team-id] :as params}] - (check-edition-permissions! pool profile-id team-id) + (check-webhook-edition-permissions! pool profile-id team-id profile-id) (validate-quotes! cfg params) (validate-webhook! cfg nil params) (insert-webhook! cfg params)) @@ -118,7 +134,7 @@ ::sm/params schema:update-webhook} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id] :as params}] (let [whook (-> (db/get pool :webhook {:id id}) (decode-row))] - (check-edition-permissions! pool profile-id (:team-id whook)) + (check-webhook-edition-permissions! pool profile-id (:team-id whook) (:profile-id whook)) (validate-webhook! cfg whook params) (update-webhook! cfg whook params))) @@ -132,15 +148,17 @@ [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id]}] (db/with-atomic [conn pool] (let [whook (-> (db/get conn :webhook {:id id}) decode-row)] - (check-edition-permissions! conn profile-id (:team-id whook)) + (check-webhook-edition-permissions! conn profile-id (:team-id whook) (:profile-id whook)) (db/delete! conn :webhook {:id id}) nil))) ;; --- Query: Webhooks (def sql:get-webhooks - "select id, uri, mtype, is_active, error_code, error_count - from webhook where team_id = ? order by uri") + "SELECT id, uri, mtype, is_active, error_code, error_count, profile_id + FROM webhook + WHERE team_id = ? + ORDER BY uri") (def ^:private schema:get-webhooks [:map {:title "get-webhooks"} diff --git a/backend/test/backend_tests/helpers.clj b/backend/test/backend_tests/helpers.clj index e77b51d6a..1e26d9810 100644 --- a/backend/test/backend_tests/helpers.clj +++ b/backend/test/backend_tests/helpers.clj @@ -332,6 +332,7 @@ (t/is (nil? (:error out))) (:result out))) + (defn create-webhook* ([params] (create-webhook* *system* params)) ([system {:keys [team-id id uri mtype is-active] diff --git a/backend/test/backend_tests/rpc_webhooks_test.clj b/backend/test/backend_tests/rpc_webhooks_test.clj index c020c5485..bc1da4c64 100644 --- a/backend/test/backend_tests/rpc_webhooks_test.clj +++ b/backend/test/backend_tests/rpc_webhooks_test.clj @@ -19,6 +19,23 @@ (t/use-fixtures :once th/state-init) (t/use-fixtures :each th/database-reset) +(defn create-webhook-params [id team] + {::th/type :create-webhook + ::rpc/profile-id id + :team-id team + :uri (u/uri "http://example.com") + :mtype "application/json"}) + +(defn check-webhook-format + ([result] + (t/is (contains? result :id)) + (t/is (contains? result :team-id)) + (t/is (contains? result :created-at)) + (t/is (contains? result :profile-id)) + (t/is (contains? result :updated-at)) + (t/is (contains? result :uri)) + (t/is (contains? result :mtype)))) + (t/deftest webhook-crud (with-mocks [http-mock {:target 'app.http.client/req! :return {:status 200}}] @@ -39,15 +56,8 @@ (t/is (nil? (:error out))) (t/is (= 1 (:call-count @http-mock))) - ;; (th/print-result! out) - (let [result (:result out)] - (t/is (contains? result :id)) - (t/is (contains? result :team-id)) - (t/is (contains? result :created-at)) - (t/is (contains? result :updated-at)) - (t/is (contains? result :uri)) - (t/is (contains? result :mtype)) + (check-webhook-format result) (t/is (= (:uri params) (:uri result))) (t/is (= (:team-id params) (:team-id result))) @@ -69,12 +79,7 @@ (t/is (= 0 (:call-count @http-mock))) (let [result (:result out)] - (t/is (contains? result :id)) - (t/is (contains? result :team-id)) - (t/is (contains? result :created-at)) - (t/is (contains? result :updated-at)) - (t/is (contains? result :uri)) - (t/is (contains? result :mtype)) + (check-webhook-format result) (t/is (= (:id params) (:id result))) (t/is (= (:id @whook) (:id result))) @@ -130,13 +135,14 @@ (let [rows (th/db-exec! ["select * from webhook"])] (t/is (= 0 (count rows)))))) - (t/testing "delete webhook (unauthorozed)" + (th/reset-mock! http-mock) + + (t/testing "delete webhook (unauthorized)" (let [params {::th/type :delete-webhook ::rpc/profile-id uuid/zero :id (:id @whook)} out (th/command! params)] - ;; (th/print-result! out) (t/is (= 0 (:call-count @http-mock))) (let [error (:error out) error-data (ex-data error)] @@ -144,6 +150,124 @@ (t/is (= (:type error-data) :not-found)) (t/is (= (:code error-data) :object-not-found)))))))) +(t/deftest webhooks-permissions-crud-viewer-only + (with-mocks [http-mock {:target 'app.http.client/req! + :return {:status 200}}] + (let [owner (th/create-profile* 1 {:is-active true}) + viewer (th/create-profile* 2 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id owner)}) + whook (volatile! nil)] + (th/create-team-role* {:team-id (:id team) + :profile-id (:id viewer) + :role :viewer}) + ;; Assert all roles for team + (let [roles (th/db-query :team-profile-rel {:team-id (:id team)})] + (t/is (= 2 (count roles)))) + + (t/testing "viewer creates a webhook" + (let [viewers-webhook (create-webhook-params (:id viewer) (:id team)) + out (th/command! viewers-webhook)] + (t/is (nil? (:error out))) + (t/is (= 1 (:call-count @http-mock))) + + (let [result (:result out)] + (check-webhook-format result) + (t/is (= (:uri viewers-webhook) (:uri result))) + (t/is (= (:team-id viewers-webhook) (:team-id result))) + (t/is (= (::rpc/profile-id viewers-webhook) (:profile-id result))) + (t/is (= (:mtype viewers-webhook) (:mtype result))) + (vreset! whook result)))) + + (th/reset-mock! http-mock) + + (t/testing "viewer updates it's own webhook (success)" + (let [params {::th/type :update-webhook + ::rpc/profile-id (:id viewer) + :id (:id @whook) + :uri (:uri @whook) + :mtype "application/transit+json" + :is-active false} + out (th/command! params) + result (:result out)] + + (t/is (nil? (:error out))) + (t/is (= 0 (:call-count @http-mock))) + (check-webhook-format result) + (t/is (= (:is-active params) (:is-active result))) + (t/is (= (:team-id @whook) (:team-id result))) + (t/is (= (:mtype params) (:mtype result))) + (vreset! whook result))) + + (th/reset-mock! http-mock) + + (t/testing "viewer deletes it's own webhook (success)" + (let [params {::th/type :delete-webhook + ::rpc/profile-id (:id viewer) + :id (:id @whook)} + out (th/command! params)] + (t/is (= 0 (:call-count @http-mock))) + (t/is (nil? (:error out))) + (t/is (nil? (:result out))) + (let [rows (th/db-exec! ["select * from webhook"])] + (t/is (= 0 (count rows)))))) + + (th/reset-mock! http-mock)))) + +(t/deftest webhooks-permissions-crud-viewer-owner + (with-mocks [http-mock {:target 'app.http.client/req! + :return {:status 200}}] + (let [owner (th/create-profile* 1 {:is-active true}) + viewer (th/create-profile* 2 {:is-active true}) + team (th/create-team* 1 {:profile-id (:id owner)}) + whook (volatile! nil)] + (th/create-team-role* {:team-id (:id team) + :profile-id (:id viewer) + :role :viewer}) + (t/testing "owner creates a wehbook" + (let [owners-webhook (create-webhook-params (:id owner) (:id team)) + out (th/command! owners-webhook) + result (:result out)] + (t/is (nil? (:error out))) + (t/is (= 1 (:call-count @http-mock))) + (check-webhook-format result) + (t/is (= (:uri owners-webhook) (:uri result))) + (t/is (= (:team-id owners-webhook) (:team-id result))) + (t/is (= (:mtype owners-webhook) (:mtype result))) + (vreset! whook result))) + + (th/reset-mock! http-mock) + + (t/testing "viewer updates owner's webhook (unauthorized)" + (let [params {::th/type :update-webhook + ::rpc/profile-id (:id viewer) + :id (:id @whook) + :uri (str (:uri @whook) "/test") + :mtype "application/transit+json" + :is-active false} + out (th/command! params)] + + (t/is (= 0 (:call-count @http-mock))) + + (let [error (:error out) + error-data (ex-data error)] + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :not-found)) + (t/is (= (:code error-data) :object-not-found))))) + + (th/reset-mock! http-mock) + + (t/testing "viewer deletes owner's webhook (unauthorized)" + (let [params {::th/type :delete-webhook + ::rpc/profile-id (:id viewer) + :id (:id @whook)} + out (th/command! params) + error (:error out) + error-data (ex-data error)] + (t/is (= 0 (:call-count @http-mock))) + (t/is (th/ex-info? error)) + (t/is (= (:type error-data) :not-found)) + (t/is (= (:code error-data) :object-not-found))))))) + (t/deftest webhooks-quotes (with-mocks [http-mock {:target 'app.http.client/req! :return {:status 200}}] diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index ace323cd1..185c727d8 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -23,6 +23,7 @@ [app.main.ui.components.forms :as fm] [app.main.ui.dashboard.change-owner] [app.main.ui.dashboard.team-form] + [app.main.ui.ds.foundations.assets.icon :refer [icon*]] [app.main.ui.icons :as i] [app.main.ui.notifications.badge :refer [badge-notification]] [app.main.ui.notifications.context-notification :refer [context-notification]] @@ -143,7 +144,7 @@ team-id (:id team) initial (mf/with-memo [team-id] - {:role "viewer" :team-id team-id}) + {:role "editor" :team-id team-id}) form (fm/use-form :schema schema:invite-member-form :initial initial) @@ -908,22 +909,25 @@ (mf/defc webhook-actions {::mf/wrap-props false} - [{:keys [on-edit on-delete]}] + [{: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? + [:* + [:button {:class (stl/css :menu-btn) + :on-click on-show} + menu-icon] + [:& dropdown {:show @show? :on-close on-hide} + [:ul {:class (stl/css :webhook-actions-dropdown)} + [:li {:on-click on-edit + :class (stl/css :webhook-dropdown-item)} (tr "labels.edit")] + [:li {:on-click on-delete + :class (stl/css :webhook-dropdown-item)} (tr "labels.delete")]]]] - - [:* - [:button {:class (stl/css :menu-btn) - :on-click on-show} - menu-icon] - [:& dropdown {:show @show? :on-close on-hide} - [:ul {:class (stl/css :webhook-actions-dropdown)} - [:li {:on-click on-edit - :class (stl/css :webhook-dropdown-item)} (tr "labels.edit")] - [:li {:on-click on-delete - :class (stl/css :webhook-dropdown-item)} (tr "labels.delete")]]]])) + [:span {:title (tr "dashboard.webhooks.cant-edit") + :class (stl/css :menu-disabled)} + [:> icon* {:id "menu"}]]))) (mf/defc last-delivery-icon {::mf/wrap-props false} @@ -936,10 +940,14 @@ (mf/defc webhook-item {::mf/wrap [mf/memo]} - [{:keys [webhook] :as props}] + [{:keys [webhook permissions] :as props}] (let [error-code (:error-code webhook) id (:id webhook) - + creator-id (:profile-id webhook) + profile (mf/deref refs/profile) + user-id (:id profile) + can-edit? (or (:can-edit permissions) + (= creator-id user-id)) on-edit (mf/use-fn (mf/deps webhook) @@ -992,14 +1000,15 @@ [:div {:class (stl/css :table-field :actions)} [:& webhook-actions {:on-edit on-edit + :can-edit? can-edit? :on-delete on-delete}]]])) (mf/defc webhooks-list {::mf/wrap-props false} - [{:keys [webhooks]}] + [{:keys [webhooks permissions]}] [:div {:class (stl/css :table-rows :webhook-table)} (for [webhook webhooks] - [:& webhook-item {:webhook webhook :key (:id webhook)}])]) + [:& webhook-item {:webhook webhook :key (:id webhook) :permissions permissions}])]) (mf/defc team-webhooks-page {::mf/wrap-props false} @@ -1025,7 +1034,7 @@ [:div {:class (stl/css :webhooks-empty)} [:div (tr "dashboard.webhooks.empty.no-webhooks")] [:div (tr "dashboard.webhooks.empty.add-one")]] - [:& webhooks-list {:webhooks webhooks}])]]])) + [:& webhooks-list {:webhooks webhooks :permissions (:permissions team)}])]]])) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; SETTINGS SECTION diff --git a/frontend/src/app/main/ui/dashboard/team.scss b/frontend/src/app/main/ui/dashboard/team.scss index d914ea773..f22e4e85f 100644 --- a/frontend/src/app/main/ui/dashboard/team.scss +++ b/frontend/src/app/main/ui/dashboard/team.scss @@ -252,7 +252,7 @@ // MEMBER ACTIONS .menu-icon { @extend .button-icon; - stroke: var(--icon-foreground); + stroke: var(--color-foreground-primary); } .menu-btn { @@ -405,6 +405,14 @@ position: relative; } +.menu-disabled { + color: var(--icon-foreground); + width: $s-28; + display: flex; + justify-content: center; + align-items: center; +} + .webhook-actions-dropdown { @extend .menu-dropdown; right: calc(-1 * $s-16); diff --git a/frontend/src/app/main/ui/onboarding/team_choice.cljs b/frontend/src/app/main/ui/onboarding/team_choice.cljs index f54d4d48e..4c22e6dcd 100644 --- a/frontend/src/app/main/ui/onboarding/team_choice.cljs +++ b/frontend/src/app/main/ui/onboarding/team_choice.cljs @@ -69,7 +69,7 @@ {::mf/props :obj} [{:keys [name on-back go-to-team?]}] (let [initial (mf/use-memo - #(do {:role "viewer" + #(do {:role "editor" :name name})) form (fm/use-form :schema schema:invite-form diff --git a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs index f73417803..fb2c6bc37 100644 --- a/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs +++ b/frontend/src/app/main/ui/workspace/sidebar/sitemap.cljs @@ -222,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])] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 53979c6b1..cc3f836a4 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -952,6 +952,10 @@ msgstr "No webhooks created so far." msgid "dashboard.webhooks.update.success" msgstr "Webhook updated successfully." +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.webhooks.cant-edit" +msgstr "You only can delete or modify webhooks created by you." + #: src/app/main/ui/settings.cljs:31 msgid "dashboard.your-account-title" msgstr "Your account" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 53ca42684..9b79b3e67 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -962,6 +962,10 @@ msgstr "No hay ningún webhook aún." msgid "dashboard.webhooks.update.success" msgstr "Webhook modificado con éxito." +#: src/app/main/ui/dashboard/team.cljs +msgid "dashboard.webhooks.cant-edit" +msgstr "Sólo puedes borrar o modificar webhooks creados por ti." + #: src/app/main/ui/settings.cljs:31 msgid "dashboard.your-account-title" msgstr "Tu cuenta" From 6fb65de1005ddeae5ff4827523058b930223d161 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Tue, 15 Oct 2024 13:31:29 +0200 Subject: [PATCH 5/8] :sparkles: Close menus and modals on role change --- frontend/src/app/main/data/dashboard.cljs | 12 +++++++- .../main/data/workspace/notifications.cljs | 4 ++- frontend/src/app/main/ui/dashboard/team.cljs | 28 ++++++++++--------- .../src/app/main/ui/workspace/main_menu.cljs | 13 +++++++++ 4 files changed, 42 insertions(+), 15 deletions(-) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index ead095adb..a6b5c4903 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -20,6 +20,7 @@ [app.main.data.events :as ev] [app.main.data.fonts :as df] [app.main.data.media :as di] + [app.main.data.modal :as modal] [app.main.data.users :as du] [app.main.data.websocket :as dws] [app.main.features :as features] @@ -1210,10 +1211,19 @@ ;; Notifications ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defn- handle-change-team-permissions-dashboard + [msg] + (ptk/reify ::handle-change-team-permissions-dashboard + ptk/WatchEvent + (watch [_ _ _] + (rx/of (dc/change-team-permissions (assoc msg :workspace? false)) + (modal/hide))))) + (defn- process-message [{:keys [type] :as msg}] (case type :notification (dc/handle-notification msg) - :team-permissions-change (dc/change-team-permissions (assoc msg :workspace? false)) + :team-permissions-change (handle-change-team-permissions-dashboard msg) :removed-from-team (dc/removed-from-team msg) nil)) \ No newline at end of file diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 85093bc74..793a3ef1a 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -13,6 +13,7 @@ [app.common.uuid :as uuid] [app.main.data.changes :as dch] [app.main.data.common :as dc] + [app.main.data.modal :as modal] [app.main.data.websocket :as dws] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.edition :as dwe] @@ -112,7 +113,8 @@ ;; Delay so anything that launched :interrupt can finish (rx/delay 100)) (if viewer? - (rx/of (dwly/set-options-mode :inspect)) + (rx/of (modal/hide) + (dwly/set-options-mode :inspect)) (rx/of (dwly/set-options-mode :design)))))))) diff --git a/frontend/src/app/main/ui/dashboard/team.cljs b/frontend/src/app/main/ui/dashboard/team.cljs index 185c727d8..391fdccec 100644 --- a/frontend/src/app/main/ui/dashboard/team.cljs +++ b/frontend/src/app/main/ui/dashboard/team.cljs @@ -313,22 +313,24 @@ on-show (mf/use-fn #(reset! show? true)) on-hide (mf/use-fn #(reset! show? false))] - [:* - (when (or is-you? (and can-delete? (not (and is-owner? (not owner?))))) + + (when (or is-you? (and can-delete? (not (and is-owner? (not owner?))))) + [:* [:button {:class (stl/css :menu-btn) :on-click on-show} - menu-icon]) + menu-icon] - [:& dropdown {:show @show? :on-close on-hide} - [:ul {:class (stl/css :actions-dropdown)} - (when is-you? - [:li {:on-click on-leave - :class (stl/css :action-dropdown-item) - :key "is-you-option"} (tr "dashboard.leave-team")]) - (when (and can-delete? (not is-you?) (not (and is-owner? (not owner?)))) - [:li {:on-click on-delete - :class (stl/css :action-dropdown-item) - :key "is-not-you-option"} (tr "labels.remove-member")])]]])) + + [:& dropdown {:show @show? :on-close on-hide} + [:ul {:class (stl/css :actions-dropdown)} + (when is-you? + [:li {:on-click on-leave + :class (stl/css :action-dropdown-item) + :key "is-you-option"} (tr "dashboard.leave-team")]) + (when (and can-delete? (not is-you?) (not (and is-owner? (not owner?)))) + [:li {:on-click on-delete + :class (stl/css :action-dropdown-item) + :key "is-not-you-option"} (tr "labels.remove-member")])]]]))) (defn- set-role! [member-id role] (let [params {:member-id member-id :role role}] diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index ec2516362..019d0a197 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -35,6 +35,7 @@ [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] [app.util.router :as rt] + [beicon.v2.core :as rx] [potok.v2.core :as ptk] [rumext.v2 :as mf])) @@ -678,6 +679,12 @@ (dom/stop-propagation event) (reset! sub-menu* nil))) + close-all-menus + (mf/use-fn + (fn [] + (reset! show-menu* false) + (reset! sub-menu* nil))) + on-menu-click (mf/use-fn (fn [event] @@ -716,6 +723,12 @@ (ptk/event ::ev/event {::ev/name "open-plugins-manager" ::ev/origin "workspace:menu"}) (modal/show :plugin-management {}))))] + (mf/with-effect [] + (let [disposable (->> st/stream + (rx/filter #(= :interrupt %)) + (rx/subs! close-all-menus))] + (partial rx/dispose! disposable))) + [:* [:div {:on-click open-menu From 536c25c2068b681e6981b640bc0557acb8863879 Mon Sep 17 00:00:00 2001 From: Eva Marco Date: Wed, 16 Oct 2024 10:37:52 +0200 Subject: [PATCH 6/8] :recycle: Resolve minor errors on viewer role on dashboard --- backend/src/app/rpc/commands/projects.clj | 2 +- .../test/backend_tests/rpc_project_test.clj | 2 +- .../main/data/workspace/notifications.cljs | 6 +---- frontend/src/app/main/ui/dashboard/fonts.cljs | 22 ++++++++++--------- frontend/src/app/main/ui/dashboard/fonts.scss | 3 +++ .../app/main/ui/dashboard/placeholder.cljs | 2 +- frontend/translations/en.po | 15 ++++--------- frontend/translations/es.po | 17 ++++---------- 8 files changed, 27 insertions(+), 42 deletions(-) diff --git a/backend/src/app/rpc/commands/projects.clj b/backend/src/app/rpc/commands/projects.clj index 1b8310232..f3532f27f 100644 --- a/backend/src/app/rpc/commands/projects.clj +++ b/backend/src/app/rpc/commands/projects.clj @@ -222,7 +222,7 @@ ::webhooks/event? true} [{:keys [::db/pool] :as cfg} {:keys [::rpc/profile-id id team-id is-pinned] :as params}] (db/with-atomic [conn pool] - (check-edition-permissions! conn profile-id id) + (check-read-permissions! conn profile-id id) (db/exec-one! conn [sql:update-project-pin team-id id profile-id is-pinned is-pinned]) nil)) diff --git a/backend/test/backend_tests/rpc_project_test.clj b/backend/test/backend_tests/rpc_project_test.clj index f35105a97..4613e6257 100644 --- a/backend/test/backend_tests/rpc_project_test.clj +++ b/backend/test/backend_tests/rpc_project_test.clj @@ -152,7 +152,7 @@ (t/is (th/ex-info? error)) (t/is (th/ex-of-type? error :not-found)))) -(t/deftest permissions-checks-delete-project +(t/deftest permissions-checks-pin-project (let [profile1 (th/create-profile* 1) profile2 (th/create-profile* 2) project (th/create-project* 1 {:team-id (:default-team-id profile1) diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index 793a3ef1a..f670993ef 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -284,8 +284,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)))))) \ No newline at end of file diff --git a/frontend/src/app/main/ui/dashboard/fonts.cljs b/frontend/src/app/main/ui/dashboard/fonts.cljs index c6c5d59af..9c2f09cf2 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.cljs +++ b/frontend/src/app/main/ui/dashboard/fonts.cljs @@ -361,7 +361,8 @@ [:div {:class (stl/css :table-field :variants)} (for [{:keys [id] :as item} variants] - [:div {:class (stl/css :variant) + [:div {:class (stl/css-case :variant true + :inhert-variant you-viewer?) :key (dm/str id)} [:span {:class (stl/css :label)} [:& font-variant-display-name {:variant item}]] @@ -384,16 +385,17 @@ :on-click on-cancel} i/close]] - (when-not you-viewer? [: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 you-viewer?] :as props}] diff --git a/frontend/src/app/main/ui/dashboard/fonts.scss b/frontend/src/app/main/ui/dashboard/fonts.scss index 4bfea724e..a49a10cbf 100644 --- a/frontend/src/app/main/ui/dashboard/fonts.scss +++ b/frontend/src/app/main/ui/dashboard/fonts.scss @@ -157,6 +157,9 @@ } } } + .inhert-variant { + cursor: default; + } } .table-field { diff --git a/frontend/src/app/main/ui/dashboard/placeholder.cljs b/frontend/src/app/main/ui/dashboard/placeholder.cljs index 00162f1c0..f49b795ae 100644 --- a/frontend/src/app/main/ui/dashboard/placeholder.cljs +++ b/frontend/src/app/main/ui/dashboard/placeholder.cljs @@ -33,7 +33,7 @@ :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") + [:> i18n/tr-html* {:content (tr "dashboard.empty-placeholder-libraries") :class (stl/css :placeholder-markdown) :tag-name "span"}])] diff --git a/frontend/translations/en.po b/frontend/translations/en.po index cc3f836a4..461359658 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -434,18 +434,11 @@ msgstr "Duplicate %s files" #: src/app/main/ui/dashboard/placeholder.cljs:33 #, markdown -msgid "dashboard.empty-placeholder-drafts" +msgid "dashboard.empty-placeholder-libraries" msgstr "" -"Files added to Libraries will appear here. Try sharing your files or add " +"Libraries added to the project will appear here. Try sharing your files or add " "from our [Libraries & templates](https://penpot.app/libraries-templates)." - - - - - - - #: src/app/main/ui/dashboard/placeholder.cljs msgid "dashboard.empty-placeholder-libraries-title" msgstr "No libraries yet." @@ -454,12 +447,12 @@ msgstr "No libraries yet." #, markdown msgid "dashboard.empty-placeholder-libraries-subtitle" msgstr "" -"Files added to Libraries will appear here. Try sharing your files or add " +"Libraries added to the project will appear here. Try sharing your files or add " "from our [Libraries & templates](https://penpot.app/libraries-templates)." #: src/app/main/ui/dashboard/placeholder.cljs msgid "dashboard.empty-placeholder-libraries-subtitle-viewer-role" -msgstr "Files added to Libraries will appear here." +msgstr "Libraries added to the project will appear here." #: src/app/main/ui/dashboard msgid "dashboard.empty-placeholder-drafts-title" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 9b79b3e67..ef9a8e754 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -436,20 +436,12 @@ msgstr "Duplicar %s archivos" #: src/app/main/ui/dashboard/placeholder.cljs:33 #, markdown -msgid "dashboard.empty-placeholder-drafts" +msgid "dashboard.empty-placeholder-libraries" msgstr "" -"Los archivos agregados a las bibliotecas aparecerán aquí. Si quieres probar " +"Las bibliotecas añadidas al proyecto aparecerán aquí. Si quieres probar " "con alguna plantilla ve a [Bibliotecas y " "plantillas](https://penpot.app/libraries-templates)." -#: src/app/main/ui/dashboard/placeholder.cljs:33 -msgid "dashboard.empty-placeholder-drafts-viewer-role" -msgstr "" -"Los archivos agregados a las bibliotecas aparecerán aquí." - - - - #: src/app/main/ui/dashboard/placeholder.cljs msgid "dashboard.empty-placeholder-libraries-title" msgstr "Aún no existen librerías compartidas." @@ -458,14 +450,13 @@ msgstr "Aún no existen librerías compartidas." #, markdown msgid "dashboard.empty-placeholder-libraries-subtitle" msgstr "" -"Los archivos agregados a las bibliotecas aparecerán aquí. Si quieres probar " +"Las bibliotecas añadidas al proyecto aparecerán aquí. Si quieres probar " "con alguna plantilla ve a [Bibliotecas y " "plantillas](https://penpot.app/libraries-templates)." #: src/app/main/ui/dashboard/placeholder.cljs msgid "dashboard.empty-placeholder-libraries-subtitle-viewer-role" -msgstr "Los archivos agregados a las bibliotecas aparecerán aquí." - +msgstr "Las bibliotecas añadidas al proyecto aparecerán aquí." #: src/app/main/ui/dashboard/files.cljs msgid "dashboard.empty-placeholder-drafts-title" From 66530ca86850af8b77f7483b06bed77936b7521f Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 17 Oct 2024 16:12:05 +0200 Subject: [PATCH 7/8] :recycle: Minor fixes on viewer role on workspace --- .../app/main/data/workspace/shortcuts.cljs | 2 +- .../app/main/ui/workspace/context_menu.cljs | 27 ++++--- .../src/app/main/ui/workspace/main_menu.cljs | 76 ++++++++++--------- .../main/ui/workspace/viewport/actions.cljs | 25 +++--- 4 files changed, 68 insertions(+), 62 deletions(-) diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 444ed499e..99ecf28fe 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -328,7 +328,7 @@ :toggle-focus-mode {:command "f" :tooltip "F" :subsections [:basics :tools] - :fn #(emit-when-no-readonly (dw/toggle-focus-mode))} + :fn #(st/emit! (dw/toggle-focus-mode))} ;; ITEM ALIGNMENT diff --git a/frontend/src/app/main/ui/workspace/context_menu.cljs b/frontend/src/app/main/ui/workspace/context_menu.cljs index d64b6589e..c5c5599f9 100644 --- a/frontend/src/app/main/ui/workspace/context_menu.cljs +++ b/frontend/src/app/main/ui/workspace/context_menu.cljs @@ -31,6 +31,7 @@ [app.main.store :as st] [app.main.ui.components.dropdown :refer [dropdown]] [app.main.ui.components.shape-icon :as sic] + [app.main.ui.context :as ctx] [app.main.ui.icons :as i] [app.main.ui.workspace.sidebar.assets.common :as cmm] [app.util.dom :as dom] @@ -534,16 +535,17 @@ :on-click do-duplicate}]])) (mf/defc viewport-context-menu - [] + [{:keys [read-only?]}] (let [focus (mf/deref refs/workspace-focus-selected) 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))] [:* - [:& menu-entry {:title (tr "workspace.shape.menu.paste") - :shortcut (sc/get-tooltip :paste) - :on-click do-paste}] + (when-not read-only? + [:& menu-entry {:title (tr "workspace.shape.menu.paste") + :shortcut (sc/get-tooltip :paste) + :on-click do-paste}]) [:& menu-entry {:title (tr "workspace.shape.menu.hide-ui") :shortcut (sc/get-tooltip :hide-ui) :on-click do-hide-ui}] @@ -643,7 +645,8 @@ (let [mdata (mf/deref menu-ref) top (- (get-in mdata [:position :y]) 20) left (get-in mdata [:position :x]) - dropdown-ref (mf/use-ref)] + dropdown-ref (mf/use-ref) + read-only? (mf/use-ctx ctx/workspace-read-only?)] (mf/use-effect (mf/deps mdata) @@ -666,9 +669,11 @@ :on-context-menu prevent-default} [:ul {:class (stl/css :context-list)} - (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}])]]])) + (if read-only? + [:& viewport-context-menu {:mdata mdata :read-only? read-only?}] + (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}]))]]])) diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 019d0a197..83c4fe53d 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -414,7 +414,7 @@ (mf/defc edit-menu {::mf/wrap-props false ::mf/wrap [mf/memo]} - [{:keys [on-close]}] + [{:keys [on-close user-viewer?]}] (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))] @@ -438,32 +438,34 @@ :key sc} sc])]] - [:> 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 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])]]) - [:> 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-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)} - (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} @@ -748,17 +750,16 @@ [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.file")] [: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 + :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 @@ -815,7 +816,8 @@ :edit [:& edit-menu - {:on-close close-sub-menu}] + {:on-close close-sub-menu + :user-viewer? user-viewer?}] :view [:& view-menu diff --git a/frontend/src/app/main/ui/workspace/viewport/actions.cljs b/frontend/src/app/main/ui/workspace/viewport/actions.cljs index 8aaef6be1..6f104dedf 100644 --- a/frontend/src/app/main/ui/workspace/viewport/actions.cljs +++ b/frontend/src/app/main/ui/workspace/viewport/actions.cljs @@ -240,20 +240,19 @@ (mf/deps @hover @hover-ids workspace-read-only?) (fn [event] (dom/prevent-default event) - (when-not workspace-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") - workspace-read-only?) - (let [position (dom/get-client-position event)] + ;;(when-not workspace-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")) + (let [position (dom/get-client-position event)] ;; Delayed callback because we need to wait to the previous context menu to be closed - (ts/schedule - #(st/emit! - (if (some? @hover) - (dw/show-shape-context-menu {:position position - :shape @hover - :hover-ids @hover-ids}) - (dw/show-context-menu {:position position})))))))))) + (ts/schedule + #(st/emit! + (if (and (not workspace-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?] From bd08e990808be99abb3a5404d38a052284e53b75 Mon Sep 17 00:00:00 2001 From: Pablo Alba Date: Thu, 17 Oct 2024 16:47:55 +0200 Subject: [PATCH 8/8] :recycle: Clean up and refactors of viewer role --- backend/src/app/rpc/commands/teams.clj | 5 +- common/src/app/common/types/team.cljc | 9 ++- frontend/src/app/main/data/changes.cljs | 2 +- frontend/src/app/main/data/common.cljs | 55 +++++++------------ frontend/src/app/main/data/dashboard.cljs | 6 +- .../main/data/workspace/notifications.cljs | 20 +++---- .../app/main/data/workspace/shortcuts.cljs | 2 +- .../main/data/workspace/text/shortcuts.cljs | 3 +- frontend/src/app/main/ui/dashboard.cljs | 2 +- .../src/app/main/ui/dashboard/projects.cljs | 7 ++- frontend/src/app/main/ui/workspace.cljs | 2 +- 11 files changed, 53 insertions(+), 60 deletions(-) diff --git a/backend/src/app/rpc/commands/teams.clj b/backend/src/app/rpc/commands/teams.clj index 54f523404..ffe7f4282 100644 --- a/backend/src/app/rpc/commands/teams.clj +++ b/backend/src/app/rpc/commands/teams.clj @@ -652,7 +652,7 @@ (mbus/pub! msgbus :topic member-id - :message {:type :team-permissions-change + :message {:type :team-role-change :subs-id member-id :team-id team-id :role role}) @@ -711,7 +711,8 @@ (mbus/pub! msgbus :topic member-id - :message {:type :removed-from-team + :message {:type :team-membership-change + :change :removed :subs-id member-id :team-id team-id :team-name (:name team)}) diff --git a/common/src/app/common/types/team.cljc b/common/src/app/common/types/team.cljc index 5eaa3787d..aed6f2039 100644 --- a/common/src/app/common/types/team.cljc +++ b/common/src/app/common/types/team.cljc @@ -7,4 +7,11 @@ (ns app.common.types.team) (def valid-roles - #{:owner :admin :editor :viewer}) \ No newline at end of file + #{:owner :admin :editor :viewer}) + +(def permissions-for-role + {:viewer {:can-edit false :is-admin false :is-owner false} + :editor {:can-edit true :is-admin false :is-owner false} + :admin {:can-edit true :is-admin true :is-owner false} + :owner {:can-edit true :is-admin true :is-owner true}}) + diff --git a/frontend/src/app/main/data/changes.cljs b/frontend/src/app/main/data/changes.cljs index cfc6a41fa..0f6454141 100644 --- a/frontend/src/app/main/data/changes.cljs +++ b/frontend/src/app/main/data/changes.cljs @@ -182,7 +182,7 @@ 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]))] + user-viewer? (not (dm/get-in state [:workspace-file :permissions :can-edit]))] ;; Prevent commit changes by a viewer team member (it really should never happen) (if user-viewer? diff --git a/frontend/src/app/main/data/common.cljs b/frontend/src/app/main/data/common.cljs index f59707028..136fafd6b 100644 --- a/frontend/src/app/main/data/common.cljs +++ b/frontend/src/app/main/data/common.cljs @@ -174,6 +174,14 @@ (rx/tap on-success) (rx/catch on-error)))))) +(defn- change-role-msg + [role] + (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"))) + (defn change-team-permissions [{:keys [team-id role workspace?]}] @@ -182,19 +190,7 @@ (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)))) + (rx/of (ntf/info (change-role-msg role)))) ptk/UpdateEvent (update [_ state] @@ -203,37 +199,24 @@ [: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 :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))))))) + (merge permissions (get tt/permissions-for-role role)))))))) -(defn removed-from-team - [{:keys [team-id team-name]}] +(defn team-membership-change + [{:keys [team-id team-name change]}] (dm/assert! (uuid? team-id)) - (ptk/reify ::removed-from-team + (ptk/reify ::team-membership-change ptk/WatchEvent (watch [_ state _] - (let [msg (tr "dashboard.removed-from-team" team-name)] + (when (= :removed change) + (let [msg (tr "dashboard.removed-from-team" team-name)] - (rx/concat - (rx/of (rt/nav :dashboard-projects {:team-id (get-in state [:profile :default-team-id])})) - (->> (rx/of (ntf/info msg)) + (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/delay 250))))))) + (rx/delay 250)))))))) diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index a6b5c4903..03ccc2ec7 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -1223,7 +1223,7 @@ (defn- process-message [{:keys [type] :as msg}] (case type - :notification (dc/handle-notification msg) - :team-permissions-change (handle-change-team-permissions-dashboard msg) - :removed-from-team (dc/removed-from-team msg) + :notification (dc/handle-notification msg) + :team-role-change (handle-change-team-permissions-dashboard msg) + :team-membership-change (dc/team-membership-change msg) nil)) \ No newline at end of file diff --git a/frontend/src/app/main/data/workspace/notifications.cljs b/frontend/src/app/main/data/workspace/notifications.cljs index f670993ef..449c26e6b 100644 --- a/frontend/src/app/main/data/workspace/notifications.cljs +++ b/frontend/src/app/main/data/workspace/notifications.cljs @@ -121,16 +121,16 @@ (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 (dc/handle-notification msg) - :team-permissions-change (handle-change-team-permissions (assoc msg :workspace? true)) - :removed-from-team (dc/removed-from-team 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 (dc/handle-notification msg) + :team-role-change (handle-change-team-permissions (assoc msg :workspace? true)) + :team-membership-change (dc/team-membership-change msg) nil)) (defn- handle-pointer-send diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index 99ecf28fe..03f27594c 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -45,7 +45,7 @@ (defn emit-when-no-readonly [& events] (let [file (deref refs/workspace-file) - user-viewer? (not (get-in file [:permissions :can-edit])) + user-viewer? (not (dm/get-in file [:permissions :can-edit])) read-only? (or (deref refs/workspace-read-only?) user-viewer?)] (when-not read-only? diff --git a/frontend/src/app/main/data/workspace/text/shortcuts.cljs b/frontend/src/app/main/data/workspace/text/shortcuts.cljs index d67b327d3..446aa015b 100644 --- a/frontend/src/app/main/data/workspace/text/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/text/shortcuts.cljs @@ -7,6 +7,7 @@ (ns app.main.data.workspace.text.shortcuts (:require [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.text :as txt] [app.main.data.shortcuts :as ds] [app.main.data.workspace.texts :as dwt] @@ -190,7 +191,7 @@ (defn- update-attrs-when-no-readonly [props] (let [undo-id (js/Symbol) file (deref refs/workspace-file) - user-viewer? (not (get-in file [:permissions :can-edit])) + 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) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index eff810496..37e023cc0 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -65,7 +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])) + you-viewer? (not (dm/get-in team [:permissions :can-edit])) dashboard-local (mf/deref refs/dashboard-local) file-menu-open? (:menu-open dashboard-local) diff --git a/frontend/src/app/main/ui/dashboard/projects.cljs b/frontend/src/app/main/ui/dashboard/projects.cljs index f2f5c20ee..34903f35f 100644 --- a/frontend/src/app/main/ui/dashboard/projects.cljs +++ b/frontend/src/app/main/ui/dashboard/projects.cljs @@ -7,6 +7,7 @@ (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] @@ -312,9 +313,9 @@ (sort-by :modified-at) (reverse)) 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])) + 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?) show-team-hero* (mf/use-state #(get storage/global ::show-team-hero true)) diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 75d422137..96ac09b30 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -172,7 +172,7 @@ team-id (:team-id project) file-name (:name file) - user-viewer? (not (get-in file [:permissions :can-edit])) + user-viewer? (not (dm/get-in file [:permissions :can-edit])) read-only? (or (mf/deref refs/workspace-read-only?) user-viewer?)