0
Fork 0
mirror of https://github.com/penpot/penpot.git synced 2025-02-13 02:28:18 -05:00

New plugin install workflow

This commit is contained in:
alonso.torres 2024-09-27 09:04:46 +02:00
parent a510d01136
commit d1277afee6
11 changed files with 252 additions and 41 deletions

View file

@ -941,7 +941,7 @@
(update-in [:dashboard-projects project-id :count] inc))))) (update-in [:dashboard-projects project-id :count] inc)))))
(defn create-file (defn create-file
[{:keys [project-id] :as params}] [{:keys [project-id name] :as params}]
(dm/assert! (uuid? project-id)) (dm/assert! (uuid? project-id))
(ptk/reify ::create-file (ptk/reify ::create-file
ev/Event ev/Event
@ -955,7 +955,7 @@
files (get state :dashboard-files) files (get state :dashboard-files)
unames (cfh/get-used-names files) unames (cfh/get-used-names files)
name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")) name (or name (cfh/generate-unique-name unames (str (tr "dashboard.new-file-prefix") " 1")))
features (-> (features/get-team-enabled-features state) features (-> (features/get-team-enabled-features state)
(set/difference cfeat/frontend-only-features)) (set/difference cfeat/frontend-only-features))
params (-> params params (-> params

View file

@ -0,0 +1,42 @@
;; 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.data.plugins
(:require
[app.plugins.register :as pr]
[potok.v2.core :as ptk]))
(defn open-plugin!
[{:keys [plugin-id name description host code icon permissions]}]
(try
(.ɵloadPlugin
js/window
#js {:pluginId plugin-id
:name name
:description description
:host host
:code code
:icon icon
:permissions (apply array permissions)})
(catch :default e
(.error js/console "Error" e))))
(defn delay-open-plugin
[plugin]
(ptk/reify ::delay-open-plugin
ptk/UpdateEvent
(update [_ state]
(assoc state ::open-plugin (:plugin-id plugin)))))
(defn check-open-plugin
[]
(ptk/reify ::check-open-plugin
ptk/WatchEvent
(watch [_ state _]
(when-let [pid (::open-plugin state)]
(open-plugin! (pr/get-plugin pid))
(fn [state]
(dissoc state ::open-plugin))))))

View file

@ -42,6 +42,7 @@
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.notifications :as ntf] [app.main.data.notifications :as ntf]
[app.main.data.persistence :as dps] [app.main.data.persistence :as dps]
[app.main.data.plugins :as dp]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.data.workspace.bool :as dwb] [app.main.data.workspace.bool :as dwb]
[app.main.data.workspace.collapse :as dwco] [app.main.data.workspace.collapse :as dwco]
@ -131,6 +132,7 @@
(when (and (not (boolean (-> state :profile :props :v2-info-shown))) (when (and (not (boolean (-> state :profile :props :v2-info-shown)))
(features/active-feature? state "components/v2")) (features/active-feature? state "components/v2"))
(modal/show :v2-info {})) (modal/show :v2-info {}))
(dp/check-open-plugin)
(fdf/fix-deleted-fonts) (fdf/fix-deleted-fonts)
(fbs/fix-broken-shapes))))) (fbs/fix-broken-shapes)))))

View file

@ -8,10 +8,15 @@
(:require-macros [app.main.style :as stl]) (:require-macros [app.main.style :as stl])
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm]
[app.common.spec :as us] [app.common.spec :as us]
[app.config :as cf] [app.config :as cf]
[app.main.data.dashboard :as dd] [app.main.data.dashboard :as dd]
[app.main.data.dashboard.shortcuts :as sc] [app.main.data.dashboard.shortcuts :as sc]
[app.main.data.events :as ev]
[app.main.data.modal :as modal]
[app.main.data.notifications :as notif]
[app.main.data.plugins :as dp]
[app.main.refs :as refs] [app.main.refs :as refs]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
@ -25,11 +30,17 @@
[app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page team-webhooks-page]] [app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page team-webhooks-page]]
[app.main.ui.dashboard.templates :refer [templates-section]] [app.main.ui.dashboard.templates :refer [templates-section]]
[app.main.ui.hooks :as hooks] [app.main.ui.hooks :as hooks]
[app.main.ui.workspace.plugins]
[app.plugins.register :as preg]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.http :as http]
[app.util.keyboard :as kbd] [app.util.keyboard :as kbd]
[app.util.object :as obj] [app.util.object :as obj]
[app.util.router :as rt]
[beicon.v2.core :as rx]
[goog.events :as events] [goog.events :as events]
[okulary.core :as l] [okulary.core :as l]
[potok.v2.core :as ptk]
[rumext.v2 :as mf])) [rumext.v2 :as mf]))
(defn ^boolean uuid-str? (defn ^boolean uuid-str?
@ -143,6 +154,70 @@
(def dashboard-initialized (def dashboard-initialized
(l/derived :current-team-id st/state)) (l/derived :current-team-id st/state))
(defn use-plugin-register
[plugin-url team-id project-id]
(let [navegate-file!
(fn [plugin {:keys [project-id id data]}]
(st/emit!
(dp/delay-open-plugin plugin)
(rt/nav :workspace
{:project-id project-id :file-id id}
{:page-id (dm/get-in data [:pages 0])})))
create-file!
(fn [plugin]
(st/emit!
(modal/hide)
(let [data
(with-meta
{:project-id project-id
:name (dm/str "Try plugin: " (:name plugin))}
{:on-success (partial navegate-file! plugin)})]
(-> (dd/create-file data)
(with-meta {::ev/origin "plugin-try-out"})))))
open-try-out-dialog
(fn [plugin]
(modal/show
:plugin-try-out
{:plugin plugin
:on-accept #(create-file! plugin)
:on-close #(modal/hide!)}))
open-permissions-dialog
(fn [plugin]
(modal/show!
:plugin-permissions
{:plugin plugin
:on-accept
#(do (preg/install-plugin! plugin)
(st/emit! (modal/hide)
(rt/nav :dashboard-projects {:team-id team-id})
(open-try-out-dialog plugin)))
:on-close
#(st/emit! (modal/hide)
(rt/nav :dashboard-projects {:team-id team-id}))}))]
(mf/with-layout-effect
[plugin-url team-id project-id]
(when plugin-url
(->> (http/send! {:method :get
:uri plugin-url
:omit-default-headers true
:response-type :json})
(rx/map :body)
(rx/subs!
(fn [body]
(if-let [plugin (preg/parse-manifest plugin-url body)]
(do
(st/emit! (ptk/event ::ev/event {::ev/name "install-plugin" :name (:name plugin) :url plugin-url}))
(open-permissions-dialog plugin))
(st/emit! (notif/error "Cannot parser the plugin manifest"))))
(fn [_]
(st/emit! (notif/error "The plugin URL is incorrect")))))))))
(mf/defc dashboard (mf/defc dashboard
{::mf/props :obj} {::mf/props :obj}
[{:keys [route profile]}] [{:keys [route profile]}]
@ -150,8 +225,12 @@
params (parse-params route) params (parse-params route)
project-id (:project-id params) project-id (:project-id params)
team-id (:team-id params) team-id (:team-id params)
search-term (:search-term params) search-term (:search-term params)
plugin-url (-> route :query-params :plugin)
invite-email (-> route :query-params :invite-email) invite-email (-> route :query-params :invite-email)
teams (mf/deref refs/teams) teams (mf/deref refs/teams)
@ -160,6 +239,8 @@
projects (mf/deref refs/dashboard-projects) projects (mf/deref refs/dashboard-projects)
project (get projects project-id) project (get projects project-id)
default-project (->> projects vals (d/seek :is-default))
initialized? (mf/deref dashboard-initialized)] initialized? (mf/deref dashboard-initialized)]
(hooks/use-shortcuts ::dashboard sc/shortcuts) (hooks/use-shortcuts ::dashboard sc/shortcuts)
@ -178,6 +259,8 @@
(fn [] (fn []
(events/unlistenByKey key)))) (events/unlistenByKey key))))
(use-plugin-register plugin-url team-id (:id default-project))
[:& (mf/provider ctx/current-team-id) {:value team-id} [:& (mf/provider ctx/current-team-id) {:value team-id}
[:& (mf/provider ctx/current-project-id) {:value project-id} [:& (mf/provider ctx/current-project-id) {:value project-id}
;; NOTE: dashboard events and other related functions assumes ;; NOTE: dashboard events and other related functions assumes
@ -206,4 +289,3 @@
:search-term search-term :search-term search-term
:team team :team team
:invite-email invite-email}])])]])) :invite-email invite-email}])])]]))

View file

@ -16,6 +16,7 @@
[app.util.router :as rt] [app.util.router :as rt]
[beicon.v2.core :as rx] [beicon.v2.core :as rx]
[cljs.spec.alpha :as s] [cljs.spec.alpha :as s]
[cuerdas.core :as str]
[potok.v2.core :as ptk])) [potok.v2.core :as ptk]))
(s/def ::page-id ::us/uuid) (s/def ::page-id ::us/uuid)
@ -94,10 +95,17 @@
(defn on-navigate (defn on-navigate
[router path] [router path]
(let [location (.-location js/document) (let [location (.-location js/document)
[base-path qs] (str/split path "?")
qstring
(->> (str/split qs "&")
(map #(str/split % "="))
(into {}))
location-path (dm/str (.-origin location) (.-pathname location)) location-path (dm/str (.-origin location) (.-pathname location))
valid-location? (= location-path (dm/str cf/public-uri)) valid-location? (= location-path (dm/str cf/public-uri))
match (match-path router path) match (match-path router path)
empty-path? (or (= path "") (= path "/"))] empty-path? (or (= base-path "") (= base-path "/"))]
(cond (cond
(not valid-location?) (not valid-location?)
(st/emit! (rt/assign-exception {:type :not-found})) (st/emit! (rt/assign-exception {:type :not-found}))
@ -116,7 +124,7 @@
(st/emit! (rt/nav :auth-login)) (st/emit! (rt/nav :auth-login))
empty-path? empty-path?
(st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)})) (st/emit! (rt/nav :dashboard-projects {:team-id (du/get-current-team-id profile)} qstring))
:else :else
(st/emit! (rt/assign-exception {:type :not-found}))))))))) (st/emit! (rt/assign-exception {:type :not-found})))))))))

View file

@ -16,6 +16,7 @@
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.exports :as de] [app.main.data.exports :as de]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.plugins :as dp]
[app.main.data.shortcuts :as scd] [app.main.data.shortcuts :as scd]
[app.main.data.users :as du] [app.main.data.users :as du]
[app.main.data.workspace :as dw] [app.main.data.workspace :as dw]
@ -29,7 +30,6 @@
[app.main.ui.context :as ctx] [app.main.ui.context :as ctx]
[app.main.ui.hooks.resize :as r] [app.main.ui.hooks.resize :as r]
[app.main.ui.icons :as i] [app.main.ui.icons :as i]
[app.main.ui.workspace.plugins :as uwp]
[app.plugins.register :as preg] [app.plugins.register :as preg]
[app.util.dom :as dom] [app.util.dom :as dom]
[app.util.i18n :as i18n :refer [tr]] [app.util.i18n :as i18n :refer [tr]]
@ -637,7 +637,7 @@
::ev/origin "workspace:menu" ::ev/origin "workspace:menu"
:name name :name name
:host host})) :host host}))
(uwp/open-plugin! manifest)) (dp/open-plugin! manifest))
:class (stl/css :submenu-item) :class (stl/css :submenu-item)
:on-key-down (fn [event] :on-key-down (fn [event]
(when (kbd/enter? event) (when (kbd/enter? event)
@ -646,7 +646,7 @@
::ev/origin "workspace:menu" ::ev/origin "workspace:menu"
:name name :name name
:host host})) :host host}))
(uwp/open-plugin! manifest))))} (dp/open-plugin! manifest))))}
[:span {:class (stl/css :item-name)} name]])]))) [:span {:class (stl/css :item-name)} name]])])))
(mf/defc menu (mf/defc menu

View file

@ -12,6 +12,7 @@
[app.config :as cf] [app.config :as cf]
[app.main.data.events :as ev] [app.main.data.events :as ev]
[app.main.data.modal :as modal] [app.main.data.modal :as modal]
[app.main.data.plugins :as dp]
[app.main.store :as st] [app.main.store :as st]
[app.main.ui.components.search-bar :refer [search-bar]] [app.main.ui.components.search-bar :refer [search-bar]]
[app.main.ui.components.title-bar :refer [title-bar]] [app.main.ui.components.title-bar :refer [title-bar]]
@ -59,22 +60,6 @@
[:button {:class (stl/css :trash-button) [:button {:class (stl/css :trash-button)
:on-click handle-delete-click} i/delete]])) :on-click handle-delete-click} i/delete]]))
(defn open-plugin!
[{:keys [plugin-id name description host code icon permissions]}]
(try
(.ɵloadPlugin
js/window
#js {:pluginId plugin-id
:name name
:description description
:host host
:code code
:icon icon
:permissions (apply array permissions)})
(catch :default e
(.error js/console "Error" e))))
(mf/defc plugin-management-dialog (mf/defc plugin-management-dialog
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :plugin-management} ::mf/register-as :plugin-management}
@ -144,7 +129,7 @@
::ev/origin "workspace:plugins" ::ev/origin "workspace:plugins"
:name (:name manifest) :name (:name manifest)
:host (:host manifest)})) :host (:host manifest)}))
(open-plugin! manifest) (dp/open-plugin! manifest)
(modal/hide!))) (modal/hide!)))
handle-remove-plugin handle-remove-plugin
@ -215,7 +200,7 @@
(mf/defc plugins-permissions-dialog (mf/defc plugins-permissions-dialog
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :plugin-permissions} ::mf/register-as :plugin-permissions}
[{:keys [plugin on-accept]}] [{:keys [plugin on-accept on-close]}]
(let [{:keys [host permissions]} plugin (let [{:keys [host permissions]} plugin
permissions (set permissions) permissions (set permissions)
@ -224,25 +209,26 @@
(mf/use-callback (mf/use-callback
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(st/emit! (modal/hide)) (st/emit! (ptk/event ::ev/event {::ev/name "allow-plugin-permissions"
(ptk/event ::ev/event {::ev/name "allow-plugin-permissions" :host host
:host host :permissions (->> permissions (str/join ", "))})
:permissions (->> permissions (str/join ", "))}) (modal/hide))
(on-accept))) (when on-accept (on-accept))))
handle-close-dialog handle-close-dialog
(mf/use-callback (mf/use-callback
(fn [event] (fn [event]
(dom/prevent-default event) (dom/prevent-default event)
(ptk/event ::ev/event {::ev/name "reject-plugin-permissions" (st/emit! (ptk/event ::ev/event {::ev/name "reject-plugin-permissions"
:host host :host host
:permissions (->> permissions (str/join ", "))}) :permissions (->> permissions (str/join ", "))})
(st/emit! (modal/hide))))] (modal/hide))
(when on-close (on-close))))]
[:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog :plugin-permissions)} [:div {:class (stl/css :modal-dialog :plugin-permissions)}
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon] [: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-title)} (tr "workspace.plugins.permissions.title" (str/upper (:name plugin)))]
[:div {:class (stl/css :modal-content)} [:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :permissions-list)} [:div {:class (stl/css :permissions-list)}
@ -293,7 +279,7 @@
(tr "workspace.plugins.permissions.comment-read")]]) (tr "workspace.plugins.permissions.comment-read")]])
(cond (cond
(contains? permissions "allow:download") (contains? permissions "allow:downloads")
[:div {:class (stl/css :permissions-list-entry)} [:div {:class (stl/css :permissions-list-entry)}
i/oauth-1 i/oauth-1
[:p {:class (stl/css :permissions-list-text)} [:p {:class (stl/css :permissions-list-text)}
@ -315,3 +301,55 @@
:type "button" :type "button"
:value (tr "ds.confirm-allow") :value (tr "ds.confirm-allow")
:on-click handle-accept-dialog}]]]]])) :on-click handle-accept-dialog}]]]]]))
(mf/defc plugins-try-out-dialog
{::mf/register modal/components
::mf/register-as :plugin-try-out}
[{:keys [plugin on-accept on-close]}]
(let [{:keys [icon host name]} plugin
handle-accept-dialog
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(st/emit! (ptk/event ::ev/event {::ev/name "try-out-accept"})
(modal/hide))
(when on-accept (on-accept))))
handle-close-dialog
(mf/use-callback
(fn [event]
(dom/prevent-default event)
(st/emit! (ptk/event ::ev/event {::ev/name "try-out-cancel"})
(modal/hide))
(when on-close (on-close))))]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-dialog :plugin-try-out)}
[:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon]
[:div {:class (stl/css :modal-title)}
[:div {:class (stl/css :plugin-icon)}
[:img {:src (if (some? icon)
(dm/str host icon)
(avatars/generate {:name name}))}]]
(tr "workspace.plugins.try-out.title" (str/upper (:name plugin)))]
[:div {:class (stl/css :modal-content)}
[:div {:class (stl/css :modal-message)}
(tr "workspace.plugins.try-out.message")]]
[:div {:class (stl/css :modal-footer)}
[:div {:class (stl/css :action-buttons)}
[:input
{:class (stl/css :cancel-button :button-expand)
:type "button"
:value (tr "workspace.plugins.try-out.cancel")
:on-click handle-close-dialog}]
[:input
{:class (stl/css :primary-button :button-expand)
:type "button"
:value (tr "workspace.plugins.try-out.try")
:on-click handle-accept-dialog}]]]]]))

View file

@ -26,6 +26,11 @@
max-width: $s-472; max-width: $s-472;
} }
&.plugin-try-out {
width: $s-452;
max-width: $s-452;
}
hr { hr {
border-color: var(--color-background-tertiary); border-color: var(--color-background-tertiary);
} }
@ -48,6 +53,8 @@
@include headlineMediumTypography; @include headlineMediumTypography;
margin-block-end: $s-32; margin-block-end: $s-32;
color: var(--modal-title-foreground-color); color: var(--modal-title-foreground-color);
display: flex;
gap: $s-12;
} }
.modal-content { .modal-content {
@ -64,6 +71,11 @@
} }
} }
.modal-message {
font-size: $fs-14;
color: var(--color-foreground-secondary);
}
.primary-button { .primary-button {
@extend .button-primary; @extend .button-primary;
@include headlineSmallTypography; @include headlineSmallTypography;

View file

@ -5,7 +5,6 @@
;; Copyright (c) KALEIDOS INC ;; Copyright (c) KALEIDOS INC
(ns app.plugins.register (ns app.plugins.register
"RPC for plugins runtime."
(:require (:require
[app.common.data :as d] [app.common.data :as d]
[app.common.data.macros :as dm] [app.common.data.macros :as dm]
@ -26,6 +25,10 @@
(->> (:ids @registry) (->> (:ids @registry)
(mapv #(dm/get-in @registry [:data %])))) (mapv #(dm/get-in @registry [:data %]))))
(defn get-plugin
[id]
(dm/get-in @registry [:data id]))
(defn parse-manifest (defn parse-manifest
"Read the manifest.json defined by the plugins definition and transforms it into an "Read the manifest.json defined by the plugins definition and transforms it into an
object that will be stored in the register." object that will be stored in the register."

View file

@ -5601,12 +5601,24 @@ msgstr "Start file downloads."
#: src/app/main/ui/workspace/plugins.cljs:236 #: src/app/main/ui/workspace/plugins.cljs:236
msgid "workspace.plugins.permissions.title" msgid "workspace.plugins.permissions.title"
msgstr "THIS PLUGIN WANTS ACCESS TO:" msgstr "'%s' PLUGIN WANTS ACCESS TO:"
#: src/app/main/ui/workspace/plugins.cljs:258 #: src/app/main/ui/workspace/plugins.cljs:258
msgid "workspace.plugins.permissions.user-read" msgid "workspace.plugins.permissions.user-read"
msgstr "Read the profile information of the current user." msgstr "Read the profile information of the current user."
msgid "workspace.plugins.try-out.title"
msgstr "'%s' PLUGIN IS INSTALLED FOR YOUR USER!"
msgid "workspace.plugins.try-out.message"
msgstr "Want to take a look? It will open in a new draft for your current team. (If not, you can always find it in the installed plugins of any file.)"
msgid "workspace.plugins.try-out.cancel"
msgstr "NOT NOW"
msgid "workspace.plugins.try-out.try"
msgstr "TRY PLUGIN"
#: src/app/main/ui/workspace/plugins.cljs:192 #: src/app/main/ui/workspace/plugins.cljs:192
msgid "workspace.plugins.plugin-list-link" msgid "workspace.plugins.plugin-list-link"
msgstr "Plugins List" msgstr "Plugins List"

View file

@ -5567,7 +5567,7 @@ msgstr "Leer y modificar el contenido de sus archivos."
#: src/app/main/ui/workspace/plugins.cljs:274 #: src/app/main/ui/workspace/plugins.cljs:274
msgid "workspace.plugins.permissions.disclaimer" msgid "workspace.plugins.permissions.disclaimer"
msgstr "Tenga en cuenta que las extensiones están desarrolladas por terceros, asegursé que confía antes de conceder el permiso. Su privacidad y seguridad es importante para nosotros. Si tiene cualquier duda, contacte soporte." msgstr "Ten en cuenta que las extensiones están desarrolladas por terceros, aseguraté que confías antes de conceder el permiso. Tu privacidad y seguridad es importante para nosotros. Si tienes cualquier duda, contacta con soporte."
#: src/app/main/ui/workspace/plugins.cljs:271 #: src/app/main/ui/workspace/plugins.cljs:271
msgid "workspace.plugins.permissions.library-read" msgid "workspace.plugins.permissions.library-read"
@ -5588,12 +5588,24 @@ msgstr "Comenzar descargas de ficheros."
#: src/app/main/ui/workspace/plugins.cljs:236 #: src/app/main/ui/workspace/plugins.cljs:236
msgid "workspace.plugins.permissions.title" msgid "workspace.plugins.permissions.title"
msgstr "LA EXTENSIÓN SOLICITA PERMISO PARA ACCEDER:" msgstr "LA EXTENSIÓN '%s' SOLICITA PERMISO PARA ACCEDER:"
#: src/app/main/ui/workspace/plugins.cljs:258 #: src/app/main/ui/workspace/plugins.cljs:258
msgid "workspace.plugins.permissions.user-read" msgid "workspace.plugins.permissions.user-read"
msgstr "Leer la información del usuario actual." msgstr "Leer la información del usuario actual."
msgid "workspace.plugins.try-out.title"
msgstr "¡LA EXTENSIÓN '%s' HA SIDO INSTALADA PARA TU USUARIO!"
msgid "workspace.plugins.try-out.message"
msgstr "¿Quieres echar un vistazo?. Crearemos un nuevo borrador en tu equipo actual. (Si no, puedes encontrar los plugins instalados en cualquier fichero.)"
msgid "workspace.plugins.try-out.cancel"
msgstr "AHORA NO"
msgid "workspace.plugins.try-out.try"
msgstr "PROBAR PLUGIN"
#: src/app/main/ui/workspace/plugins.cljs:192 #: src/app/main/ui/workspace/plugins.cljs:192
msgid "workspace.plugins.plugin-list-link" msgid "workspace.plugins.plugin-list-link"
msgstr "Lista de extensiones" msgstr "Lista de extensiones"