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."