diff --git a/frontend/resources/images/icons/oauth-1.svg b/frontend/resources/images/icons/oauth-1.svg new file mode 100644 index 0000000000..49a0dec9bf --- /dev/null +++ b/frontend/resources/images/icons/oauth-1.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/images/icons/oauth-2.svg b/frontend/resources/images/icons/oauth-2.svg new file mode 100644 index 0000000000..06c59a1852 --- /dev/null +++ b/frontend/resources/images/icons/oauth-2.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/images/icons/oauth-3.svg b/frontend/resources/images/icons/oauth-3.svg new file mode 100644 index 0000000000..db38820bcf --- /dev/null +++ b/frontend/resources/images/icons/oauth-3.svg @@ -0,0 +1 @@ + diff --git a/frontend/resources/images/icons/puzzle.svg b/frontend/resources/images/icons/puzzle.svg new file mode 100644 index 0000000000..6e978bac53 --- /dev/null +++ b/frontend/resources/images/icons/puzzle.svg @@ -0,0 +1 @@ + diff --git a/frontend/src/app/config.cljs b/frontend/src/app/config.cljs index b2e3d95c4f..286a179038 100644 --- a/frontend/src/app/config.cljs +++ b/frontend/src/app/config.cljs @@ -110,6 +110,7 @@ (def privacy-policy-uri (obj/get global "penpotPrivacyPolicyURI" "https://penpot.app/privacy")) (def flex-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) (def grid-help-uri (obj/get global "penpotGridHelpURI" "https://help.penpot.app/user-guide/flexible-layouts/")) +(def plugins-list-uri (obj/get global "penpotPluginsListUri" "https://penpot-docs-plugins.netlify.app/technical-guide/plugins/getting-started/#examples")) (defn- normalize-uri [uri-str] diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index 3191194248..324eb8098c 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -178,6 +178,9 @@ (def ^:icon msg-success (icon-xref :msg-success)) (def ^:icon msg-warning (icon-xref :msg-warning)) (def ^:icon open-link (icon-xref :open-link)) +(def ^:icon oauth-1 (icon-xref :oauth-1)) +(def ^:icon oauth-2 (icon-xref :oauth-2)) +(def ^:icon oauth-3 (icon-xref :oauth-3)) (def ^:icon padding-bottom (icon-xref :padding-bottom)) (def ^:icon padding-extended (icon-xref :padding-extended)) (def ^:icon padding-left (icon-xref :padding-left)) @@ -190,6 +193,7 @@ (def ^:icon picker (icon-xref :picker)) (def ^:icon pin (icon-xref :pin)) (def ^:icon play (icon-xref :play)) +(def ^:icon puzzle (icon-xref :puzzle)) (def ^:icon rectangle (icon-xref :rectangle)) (def ^:icon reload (icon-xref :reload)) (def ^:icon remove-icon (icon-xref :remove)) diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index 2abca79a0c..d2356c3f17 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -30,6 +30,7 @@ [app.main.ui.hooks.resize :as r] [app.main.ui.icons :as i] [app.main.ui.workspace.plugins :as uwp] + [app.plugins :as plugins] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -608,7 +609,7 @@ ::mf/wrap [mf/memo]} [{:keys [open-plugins on-close]}] (when (features/active-feature? @st/state "plugins/runtime") - (let [plugins (uwp/load-from-store)] + (let [plugins (plugins/load-from-store)] [:& dropdown-menu {:show true :list-class (stl/css-case :sub-menu true :plugins true) :on-close on-close} diff --git a/frontend/src/app/main/ui/workspace/plugins.cljs b/frontend/src/app/main/ui/workspace/plugins.cljs index cf55b802d0..e940151242 100644 --- a/frontend/src/app/main/ui/workspace/plugins.cljs +++ b/frontend/src/app/main/ui/workspace/plugins.cljs @@ -9,22 +9,23 @@ (:require [app.common.data :as d] [app.common.data.macros :as dm] - [app.common.uuid :as uuid] + [app.config :as cf] [app.main.data.modal :as modal] + [app.main.store :as st] [app.main.ui.components.search-bar :refer [search-bar]] [app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.icons :as i] + [app.plugins :as plugins] [app.util.avatars :as avatars] + [app.util.dom :as dom] [app.util.http :as http] [app.util.i18n :as i18n :refer [tr]] - [app.util.object :as obj] [beicon.v2.core :as rx] [rumext.v2 :as mf])) (def ^:private close-icon (i/icon-xref :close (stl/css :close-icon))) - (mf/defc plugin-entry [{:keys [index manifest on-open-plugin on-remove-plugin]}] @@ -55,20 +56,6 @@ [:button {:class (stl/css :trash-button) :on-click handle-delete-click} i/delete]])) -(defn load-from-store - [] - (let [ls (.-localStorage js/window) - plugins-val (.getItem ls "plugins")] - (when plugins-val - (let [plugins-js (.parse js/JSON plugins-val)] - (js->clj plugins-js {:keywordize-keys true}))))) - -(defn save-to-store - [plugins] - (let [ls (.-localStorage js/window) - plugins-js (clj->js plugins) - plugins-val (.stringify js/JSON plugins-js)] - (.setItem ls "plugins" plugins-val))) (defn open-plugin! [{:keys [plugin-id name description host code icon permissions]}] @@ -123,27 +110,20 @@ (rx/map :body) (rx/subs! (fn [body] - (let [name (obj/get body "name") - desc (obj/get body "description") - code (obj/get body "code") - icon (obj/get body "icon") - permissions (obj/get body "permissions") - origin (obj/get (js/URL. plugin-url) "origin") - plugin-id (str (uuid/next)) + (let [plugin (plugins/parser-manifest plugin-url body) + new-state (vec (conj (seq plugins-state) plugin))] - new-state - (conj plugins-state - {:plugin-id plugin-id - :name name - :description desc - :host origin - :code code - :icon icon - :permissions (->> permissions (mapv str))})] (reset! input-status* :success) (reset! plugin-url* "") (reset! plugins-state* new-state) - (save-to-store new-state))) + + (modal/show! + :plugin-permissions + {:plugin plugin + :on-accept + #(do + (plugins/save-to-store new-state) + (modal/show! :plugin-management {}))}))) (fn [_] (reset! input-status* :error-url)))))) @@ -162,16 +142,15 @@ (keep-indexed (fn [idx item] (when (not= idx plugin-index) item))) plugins-state)] - (reset! plugins-state* new-state) - (save-to-store new-state))))] + (plugins/save-to-store new-state))))] (mf/use-effect (fn [] - (reset! plugins-state* (d/nilv (load-from-store) [])))) + (reset! plugins-state* (d/nilv (plugins/load-from-store) [])))) [:div {:class (stl/css :modal-overlay)} - [:div {:class (stl/css :modal-dialog)} + [:div {:class (stl/css :modal-dialog :plugin-management)} [:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon] [:div {:class (stl/css :modal-title)} (tr "workspace.plugins.title")] @@ -183,7 +162,6 @@ :class (stl/css-case :input-error error?)}] [:button {:class (stl/css :primary-button) - :disabled (empty? plugin-url) :on-click handle-install-click} (tr "workspace.plugins.install")]] (when error? @@ -194,9 +172,9 @@ (if (empty? plugins-state) [:div {:class (stl/css :plugins-empty)} - [:div {:class (stl/css :plugins-empty-logo)} i/rocket] + [:div {:class (stl/css :plugins-empty-logo)} i/puzzle] [:div {:class (stl/css :plugins-empty-text)} (tr "workspace.plugins.empty-plugins")] - [:a {:class (stl/css :plugins-link) :href "#"} + [:a {:class (stl/css :plugins-link) :href cf/plugins-list-uri :target "_blank"} (tr "workspace.plugins.plugin-list-link") i/external-link]] [:* @@ -204,10 +182,84 @@ :title (tr "workspace.plugins.installed-plugins")}] [:div {:class (stl/css :plugins-list)} - (for [[idx manifest] (d/enumerate plugins-state)] [:& plugin-entry {:key (dm/str "plugin-" idx) :index idx :manifest manifest :on-open-plugin handle-open-plugin :on-remove-plugin handle-remove-plugin}])]])]]])) + +(mf/defc plugins-permissions-dialog + {::mf/register modal/components + ::mf/register-as :plugin-permissions} + [{:keys [plugin on-accept]}] + + (let [{:keys [permissions]} plugin + permissions (set permissions) + + handle-accept-dialog + (mf/use-callback + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide)) + (on-accept))) + + handle-close-dialog + (mf/use-callback + (fn [event] + (dom/prevent-default event) + (st/emit! (modal/hide))))] + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog :plugin-permissions)} + [:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon] + [:div {:class (stl/css :modal-title)} (tr "workspace.plugins.permissions.title")] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :permissions-list)} + (when (contains? permissions "content:read") + [:div {:class (stl/css :permissions-list-entry)} + i/oauth-1 + [:p {:class (stl/css :permissions-list-text)} + (tr "workspace.plugins.permissions.disclaimer")]]) + + (when (contains? permissions "content:write") + [:div {:class (stl/css :permissions-list-entry)} + i/oauth-1 + [:p {:class (stl/css :permissions-list-text)} + (tr "workspace.plugins.permissions.content-read")]]) + + (when (contains? permissions "user:read") + [:div {:class (stl/css :permissions-list-entry)} + i/oauth-2 + [:p {:class (stl/css :permissions-list-text)} + (tr "workspace.plugins.permissions.content-write")]]) + + (when (contains? permissions "library:read") + [:div {:class (stl/css :permissions-list-entry)} + i/oauth-3 + [:p {:class (stl/css :permissions-list-text)} + (tr "workspace.plugins.permissions.user-read")]]) + + (when (contains? permissions "library:write") + [:div {:class (stl/css :permissions-list-entry)} + i/oauth-3 + [:p {:class (stl/css :permissions-list-text)} + (tr "workspace.plugins.permissions.library-read")]])] + + [:div {:class (stl/css :permissions-disclaimer)} + (tr "workspace.plugins.permissions.library-write")]] + + [:div {:class (stl/css :modal-footer)} + [:div {:class (stl/css :action-buttons)} + [:input + {:class (stl/css :cancel-button :button-expand) + :type "button" + :value (tr "ds.confirm-cancel") + :on-click handle-close-dialog}] + + [:input + {:class (stl/css :primary-button :button-expand) + :type "button" + :value (tr "ds.confirm-allow") + :on-click handle-accept-dialog}]]]]])) diff --git a/frontend/src/app/main/ui/workspace/plugins.scss b/frontend/src/app/main/ui/workspace/plugins.scss index 0ff79d4885..fd9114e813 100644 --- a/frontend/src/app/main/ui/workspace/plugins.scss +++ b/frontend/src/app/main/ui/workspace/plugins.scss @@ -13,15 +13,27 @@ .modal-dialog { @extend .modal-container-base; display: grid; - grid-template-rows: auto 1fr; - width: $s-472; - max-width: $s-472; + grid-template-rows: auto 1fr auto; + + &.plugin-permissions { + width: $s-412; + max-width: $s-412; + } + + &.plugin-management { + width: $s-472; + max-width: $s-472; + } hr { border-color: $db-tertiary; } } +.modal-footer { + margin-top: 2rem; +} + .close-btn { @extend .modal-close-btn-base; } @@ -40,8 +52,15 @@ .modal-content { display: flex; flex-direction: column; - height: $s-380; - max-height: $s-380; + + .plugin-permissions & { + gap: $s-20; + } + + .plugin-management & { + height: $s-380; + max-height: $s-380; + } } .primary-button { @@ -50,6 +69,17 @@ padding: $s-0 $s-16; } +.button-expand { + width: 100%; + margin: 0; +} + +.cancel-button { + @extend .button-secondary; + @include headlineSmallTypography; + padding: $s-0 $s-16; +} + .search-icon { @include flexCenter; width: $s-20; @@ -87,7 +117,7 @@ .plugins-list { padding-top: $s-20; overflow-x: hidden; - overflow-y: scroll; + overflow-y: auto; flex: 1; display: flex; flex-direction: column; @@ -148,8 +178,8 @@ svg { width: $s-16; height: $s-16; - fill: $df-secondary; - stroke-width: 0; + stroke: $df-secondary; + fill: none; } } @@ -190,3 +220,45 @@ div.input-error { fill: none; } } + +.plugin-permissions { +} + +.permissions-list { + display: flex; + flex-direction: column; + gap: $s-24; +} + +.permissions-list-entry { + display: grid; + grid-template-columns: 24px 1fr; + gap: $s-16; + align-items: center; + + svg { + width: $s-24; + height: $s-24; + stroke: $da-primary; + fill: none; + } +} + +.permissions-list-text { + @include bodySmallTypography; + margin: 0; + color: $df-secondary; +} + +.permissions-disclaimer { + @include bodySmallTypography; + padding: $s-16; + background: $db-tertiary; + color: $df-secondary; + border-radius: $br-4; +} + +.action-buttons { + display: flex; + gap: $s-12; +} diff --git a/frontend/src/app/main/ui/workspace/top_toolbar.cljs b/frontend/src/app/main/ui/workspace/top_toolbar.cljs index f7b0bcf990..dfbb3fe1ae 100644 --- a/frontend/src/app/main/ui/workspace/top_toolbar.cljs +++ b/frontend/src/app/main/ui/workspace/top_toolbar.cljs @@ -11,11 +11,13 @@ [app.common.geom.point :as gpt] [app.common.media :as cm] [app.main.data.events :as ev] + [app.main.data.modal :as modal] [app.main.data.workspace :as dw] [app.main.data.workspace.common :as dwc] [app.main.data.workspace.media :as dwm] [app.main.data.workspace.path.state :as pst] [app.main.data.workspace.shortcuts :as sc] + [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] @@ -192,6 +194,17 @@ :data-testid "path-btn"} i/path]] + (when (features/active-feature? @st/state "plugins/runtime") + [:li + [:button + {:title (tr "workspace.toolbar.plugins" (sc/get-tooltip :plugins)) + :aria-label (tr "workspace.toolbar.plugins" (sc/get-tooltip :plugins)) + :class (stl/css :main-toolbar-options-button) + :on-click #(modal/show! :plugin-management {}) + :data-tool "plugins" + :data-testid "plugins-btn"} + i/puzzle]]) + (when *assert* [:li [:button diff --git a/frontend/src/app/plugins.cljs b/frontend/src/app/plugins.cljs index 36346de271..7026336544 100644 --- a/frontend/src/app/plugins.cljs +++ b/frontend/src/app/plugins.cljs @@ -7,6 +7,7 @@ (ns app.plugins "RPC for plugins runtime." (:require + [app.common.uuid :as uuid] [app.main.features :as features] [app.main.store :as st] [app.plugins.api :as api] @@ -33,3 +34,35 @@ (rx/take 1) (rx/tap init-plugins-runtime!) (rx/ignore))))) + +(defn parser-manifest + [plugin-url ^js manifest] + (let [name (obj/get manifest "name") + desc (obj/get manifest "description") + code (obj/get manifest "code") + icon (obj/get manifest "icon") + permissions (obj/get manifest "permissions") + origin (obj/get (js/URL. plugin-url) "origin") + plugin-id (str (uuid/next))] + {:plugin-id plugin-id + :name name + :description desc + :host origin + :code code + :icon icon + :permissions (->> permissions (mapv str))})) + +(defn load-from-store + [] + (let [ls (.-localStorage js/window) + plugins-val (.getItem ls "plugins")] + (when plugins-val + (let [plugins-js (.parse js/JSON plugins-val)] + (js->clj plugins-js {:keywordize-keys true}))))) + +(defn save-to-store + [plugins] + (let [ls (.-localStorage js/window) + plugins-js (clj->js plugins) + plugins-val (.stringify js/JSON plugins-js)] + (.setItem ls "plugins" plugins-val))) diff --git a/frontend/src/app/plugins/events.cljs b/frontend/src/app/plugins/events.cljs index eada65d67c..27d9269484 100644 --- a/frontend/src/app/plugins/events.cljs +++ b/frontend/src/app/plugins/events.cljs @@ -6,7 +6,6 @@ (ns app.plugins.events (:require - [app.common.data.macros :as dm] [app.main.store :as st] [app.plugins.file :as file] [app.plugins.page :as page] @@ -24,25 +23,19 @@ (defmethod handle-state-change "filechange" [_ plugin-id old-val new-val] - (let [old-file (:workspace-file old-val) - new-file (:workspace-file new-val) - old-data (:workspace-data old-val) - new-data (:workspace-data new-val)] - (if (and (identical? old-file new-file) - (identical? old-data new-data)) + (let [old-file-id (:current-file-id old-val) + new-file-id (:current-file-id new-val)] + (if (identical? old-file-id new-file-id) ::not-changed - (file/file-proxy plugin-id (:id new-file))))) + (file/file-proxy plugin-id new-file-id)))) (defmethod handle-state-change "pagechange" [_ plugin-id old-val new-val] - (let [file-id (:current-file-id new-val) - old-page-id (:current-page-id old-val) - new-page-id (:current-page-id new-val) - old-page (dm/get-in old-val [:workspace-data :pages-index old-page-id]) - new-page (dm/get-in new-val [:workspace-data :pages-index new-page-id])] - (if (identical? old-page new-page) + (let [old-page-id (:current-page-id old-val) + new-page-id (:current-page-id new-val)] + (if (identical? old-page-id new-page-id) ::not-changed - (page/page-proxy plugin-id file-id new-page-id)))) + (page/page-proxy plugin-id (:current-file-id new-val) new-page-id)))) (defmethod handle-state-change "selectionchange" [_ _ old-val new-val] @@ -75,7 +68,10 @@ (fn [_ _ old-val new-val] (let [result (handle-state-change type plugin-id old-val new-val)] (when (not= ::not-changed result) - (callback result))))) + (try + (callback result) + (catch :default cause + (.error js/console cause))))))) ;; return the generated key key)) diff --git a/frontend/src/app/plugins/shape.cljs b/frontend/src/app/plugins/shape.cljs index fb5a88ff6b..bc1b53abab 100644 --- a/frontend/src/app/plugins/shape.cljs +++ b/frontend/src/app/plugins/shape.cljs @@ -60,23 +60,6 @@ (declare shape-proxy) (declare shape-proxy?) -#_(defn parse-command - [entry] - (update entry - :command - #(case % - "M" :move-to - "Z" :close-path - "L" :line-to - "H" :line-to-horizontal - "V" :line-to-vertical - "C" :curve-to - "S" :smooth-curve-to - "Q" :quadratic-bezier-curve-to - "T" :smooth-quadratic-bezier-curve-to - "A" :elliptical-arc - (keyword %)))) - (defn- shadow-defaults [shadow] (d/patch-object diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 7a972067de..8458241ba9 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -847,6 +847,9 @@ msgstr "Cancel" msgid "ds.confirm-ok" msgstr "Ok" +msgid "ds.confirm-allow" +msgstr "Allow" + #: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs msgid "ds.confirm-title" msgstr "Are you sure?" @@ -5290,3 +5293,24 @@ msgstr "Plugins manager" msgid "workspace.plugins.plugin-list-link" msgstr "Plugins List" + +msgid "workspace.plugins.permissions.title" +msgstr "THIS PLUGIN WANTS ACCESS TO:" + +msgid "workspace.plugins.permissions.disclaimer" +msgstr "Note that this plugin has been created by an external party." + +msgid "workspace.plugins.permissions.content-read" +msgstr "Read the content of files that users have access to." + +msgid "workspace.plugins.permissions.content-write" +msgstr "Read and modify the content of files that users have access to." + +msgid "workspace.plugins.permissions.user-read" +msgstr "Read the profile information of the current user." + +msgid "workspace.plugins.permissions.library-read" +msgstr "Read your libraries and assets." + +msgid "workspace.plugins.permissions.library-write" +msgstr "Read and modify your libraries and assets." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 76a2264ca0..187c38f822 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -869,6 +869,9 @@ msgstr "Cancelar" msgid "ds.confirm-ok" msgstr "Ok" +msgid "ds.confirm-allow" +msgstr "Permitir" + #: src/app/main/ui/confirm.cljs, src/app/main/ui/confirm.cljs msgid "ds.confirm-title" msgstr "¿Está Seguro?" @@ -5394,3 +5397,24 @@ msgstr "Gestor de extensiones" msgid "workspace.plugins.plugin-list-link" msgstr "Lista de extensiones" + +msgid "workspace.plugins.permissions.title" +msgstr "LA EXTENSIÓN SOLICITA PERMISO PARA ACCEDER:" + +msgid "workspace.plugins.permissions.disclaimer" +msgstr "Tenga en cuenta que esta extensión ha sido desarrollada por terceros." + +msgid "workspace.plugins.permissions.content-read" +msgstr "Leer el contenido de sus archivos." + +msgid "workspace.plugins.permissions.content-write" +msgstr "Leer y modificar el contenido de sus archivos." + +msgid "workspace.plugins.permissions.user-read" +msgstr "Leer la información del usuario actual." + +msgid "workspace.plugins.permissions.library-read" +msgstr "Leer la información de sus bibliotecas y recursos." + +msgid "workspace.plugins.permissions.library-write" +msgstr "Leer y modificar la información de sus bibliotecas y recursos."