From 4c71a4367fb8df2c37b152c9e2782523ca82af3f Mon Sep 17 00:00:00 2001 From: "alonso.torres" Date: Tue, 14 May 2024 12:11:13 +0200 Subject: [PATCH] :sparkles: Add plugins management dialog --- .../app/main/data/workspace/shortcuts.cljs | 12 +- .../app/main/ui/components/search_bar.cljs | 5 +- frontend/src/app/main/ui/workspace.cljs | 1 + .../src/app/main/ui/workspace/main_menu.cljs | 75 +++++++- .../src/app/main/ui/workspace/main_menu.scss | 10 +- .../src/app/main/ui/workspace/plugins.cljs | 180 ++++++++++++++++++ .../src/app/main/ui/workspace/plugins.scss | 167 ++++++++++++++++ frontend/src/app/plugins/api.cljs | 15 +- frontend/src/app/plugins/library.cljs | 96 ++++++++++ frontend/translations/en.po | 30 +++ frontend/translations/es.po | 31 +++ 11 files changed, 608 insertions(+), 14 deletions(-) create mode 100644 frontend/src/app/main/ui/workspace/plugins.cljs create mode 100644 frontend/src/app/main/ui/workspace/plugins.scss create mode 100644 frontend/src/app/plugins/library.cljs diff --git a/frontend/src/app/main/data/workspace/shortcuts.cljs b/frontend/src/app/main/data/workspace/shortcuts.cljs index f3d7a72d6..6ca11f33e 100644 --- a/frontend/src/app/main/data/workspace/shortcuts.cljs +++ b/frontend/src/app/main/data/workspace/shortcuts.cljs @@ -8,6 +8,7 @@ (:require [app.main.data.events :as ev] [app.main.data.exports :as de] + [app.main.data.modal :as modal] [app.main.data.preview :as dp] [app.main.data.shortcuts :as ds] [app.main.data.users :as du] @@ -23,6 +24,7 @@ [app.main.data.workspace.texts :as dwtxt] [app.main.data.workspace.transforms :as dwt] [app.main.data.workspace.undo :as dwu] + [app.main.features :as features] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.hooks.resize :as r] @@ -552,7 +554,15 @@ :command (ds/a-mod "m") :subsections [:basics] :fn #(st/emit! (with-meta (du/toggle-theme) - {::ev/origin "workspace:shortcut"}))}}) + {::ev/origin "workspace:shortcut"}))} + + + ;; PLUGINS + :plugins {:tooltip (ds/meta (ds/alt "P")) + :command (ds/c-mod "alt+p") + :subsections [:basics] + :fn #(when (features/active-feature? @st/state "plugins/runtime") + (st/emit! (modal/show :plugin-management {})))}}) (def debug-shortcuts ;; PREVIEW diff --git a/frontend/src/app/main/ui/components/search_bar.cljs b/frontend/src/app/main/ui/components/search_bar.cljs index 0fb73303c..e9c82e391 100644 --- a/frontend/src/app/main/ui/components/search_bar.cljs +++ b/frontend/src/app/main/ui/components/search_bar.cljs @@ -7,6 +7,7 @@ (ns app.main.ui.components.search-bar (:require-macros [app.main.style :as stl]) (:require + [app.common.data.macros :as dm] [app.main.ui.icons :as i] [app.util.dom :as dom] [app.util.keyboard :as kbd] @@ -23,7 +24,7 @@ icon (unchecked-get props "icon") autofocus (unchecked-get props "auto-focus") id (unchecked-get props "id") - + input-class (unchecked-get props "class") handle-change (mf/use-fn @@ -51,7 +52,7 @@ [:span {:class (stl/css-case :search-box true :has-children (some? children))} children - [:div {:class (stl/css :search-input-wrapper)} + [:div {:class (dm/str input-class " " (stl/css :search-input-wrapper))} icon [:input {:id id :on-change handle-change diff --git a/frontend/src/app/main/ui/workspace.cljs b/frontend/src/app/main/ui/workspace.cljs index 8363e438f..3f3c0f71a 100644 --- a/frontend/src/app/main/ui/workspace.cljs +++ b/frontend/src/app/main/ui/workspace.cljs @@ -26,6 +26,7 @@ [app.main.ui.workspace.libraries] [app.main.ui.workspace.nudge] [app.main.ui.workspace.palette :refer [palette]] + [app.main.ui.workspace.plugins] [app.main.ui.workspace.sidebar :refer [left-sidebar right-sidebar]] [app.main.ui.workspace.sidebar.collapsable-button :refer [collapsed-button]] [app.main.ui.workspace.sidebar.history :refer [history-toolbox]] diff --git a/frontend/src/app/main/ui/workspace/main_menu.cljs b/frontend/src/app/main/ui/workspace/main_menu.cljs index d144706c3..3f554d61e 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.cljs +++ b/frontend/src/app/main/ui/workspace/main_menu.cljs @@ -7,6 +7,8 @@ (ns app.main.ui.workspace.main-menu (:require-macros [app.main.style :as stl]) (:require + [app.common.data :as d] + [app.common.data.macros :as dm] [app.common.files.helpers :as cfh] [app.common.uuid :as uuid] [app.config :as cf] @@ -20,12 +22,14 @@ [app.main.data.workspace.common :as dwc] [app.main.data.workspace.libraries :as dwl] [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.dropdown-menu :refer [dropdown-menu dropdown-menu-item*]] [app.main.ui.context :as ctx] [app.main.ui.hooks.resize :as r] [app.main.ui.icons :as i] + [app.main.ui.workspace.plugins :as uwp] [app.util.dom :as dom] [app.util.i18n :as i18n :refer [tr]] [app.util.keyboard :as kbd] @@ -60,6 +64,9 @@ nav-to-feedback (mf/use-fn #(st/emit! (rt/nav-new-window* {:rname :settings-feedback}))) + plugins? + (features/active-feature? @st/state "plugins/runtime") + show-shortcuts (mf/use-fn (mf/deps layout) @@ -83,7 +90,8 @@ [:& dropdown-menu {:show true :on-close on-close :list-class (stl/css-case :sub-menu true - :help-info true)} + :help-info plugins? + :help-info-old (not plugins?))} [:> dropdown-menu-item* {:class (stl/css :submenu-item) :on-click nav-to-helpc-center :on-key-down (fn [event] @@ -148,7 +156,6 @@ :id "file-menu-shortcuts"} [:span {:class (stl/css :item-name)} (tr "label.shortcuts")] [:span {:class (stl/css :shortcut)} - (for [sc (scd/split-sc (sc/get-tooltip :show-shortcuts))] [:span {:class (stl/css :shortcut-key) :key sc} sc])]] @@ -596,6 +603,41 @@ [:span {:class (stl/css :item-name)} (tr "dashboard.export-frames")]])])) +(mf/defc plugins-menu + {::mf/wrap-props false + ::mf/wrap [mf/memo]} + [{:keys [open-plugins on-close]}] + (when (features/active-feature? @st/state "plugins/runtime") + (let [plugins (uwp/load-from-store)] + [:& dropdown-menu {:show true + :list-class (stl/css-case :sub-menu true :plugins true) + :on-close on-close} + [:> dropdown-menu-item* {:on-click open-plugins + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + (open-plugins event))) + :data-test "open-plugins" + :id "file-menu-open-plugins"} + [:span {:class (stl/css :item-name)} + (tr "workspace.plugins.menu.plugins-manager")] + [:span {:class (stl/css :shortcut)} + (for [sc (scd/split-sc (sc/get-tooltip :plugins))] + [:span {:class (stl/css :shortcut-key) :key sc} sc])]] + + + (when (d/not-empty? plugins) + [:div {:class (stl/css :separator)}]) + + (for [[idx {:keys [name url]}] (d/enumerate plugins)] + [:> dropdown-menu-item* {:key (dm/str "plugins-menu-" idx) + :on-click #(uwp/open-plugin! url) + :class (stl/css :submenu-item) + :on-key-down (fn [event] + (when (kbd/enter? event) + #(uwp/open-plugin! url)))} + [:span {:class (stl/css :item-name)} name]])]))) + (mf/defc menu {::mf/wrap-props false} [{:keys [layout file profile]}] @@ -644,12 +686,19 @@ (reset! show-menu* false) (reset! sub-menu* nil)))) - toggle-theme (mf/use-fn (fn [event] (dom/stop-propagation event) - (st/emit! (du/toggle-theme))))] + (st/emit! (du/toggle-theme)))) + + open-plugins-manager + (mf/use-fn + (fn [event] + (dom/stop-propagation event) + (reset! show-menu* false) + (reset! sub-menu* nil) + (st/emit! (modal/show :plugin-management {}))))] [:* @@ -703,6 +752,19 @@ :id "file-menu-preferences"} [:span {:class (stl/css :item-name)} (tr "workspace.header.menu.option.preferences")] [:span {:class (stl/css :open-arrow)} i/arrow]] + + (when (features/active-feature? @st/state "plugins/runtime") + [:> 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-test "plugins" + :id "file-menu-plugins"} + [:span {:class (stl/css :item-name)} (tr "workspace.plugins.menu.title")] + [:span {:class (stl/css :open-arrow)} i/arrow]]) + [:div {:class (stl/css :separator)}] [:> dropdown-menu-item* {:class (stl/css-case :menu-item true) :on-click on-menu-click @@ -739,6 +801,11 @@ :toggle-theme toggle-theme :on-close close-sub-menu}] + :plugins + [:& plugins-menu + {:open-plugins open-plugins-manager + :on-close close-sub-menu}] + :help-info [:& help-info-menu {:layout layout diff --git a/frontend/src/app/main/ui/workspace/main_menu.scss b/frontend/src/app/main/ui/workspace/main_menu.scss index 55732dab2..f986ced89 100644 --- a/frontend/src/app/main/ui/workspace/main_menu.scss +++ b/frontend/src/app/main/ui/workspace/main_menu.scss @@ -95,7 +95,15 @@ top: $s-148; } + &.plugins { + top: $s-180; + } + &.help-info { - top: $s-196; + top: $s-232; + } + + &.help-info-old { + top: $s-192; } } diff --git a/frontend/src/app/main/ui/workspace/plugins.cljs b/frontend/src/app/main/ui/workspace/plugins.cljs new file mode 100644 index 000000000..13a69be15 --- /dev/null +++ b/frontend/src/app/main/ui/workspace/plugins.cljs @@ -0,0 +1,180 @@ +;; 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.workspace.plugins + (:require-macros [app.main.style :as stl]) + (:require + [app.common.data :as d] + [app.common.data.macros :as dm] + [app.main.data.modal :as modal] + [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.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 _icon url name description on-open-plugin on-remove-plugin]}] + + (let [handle-open-click + (mf/use-callback + (mf/deps index url on-open-plugin) + (fn [] + (when on-open-plugin + (on-open-plugin index url)))) + + handle-delete-click + (mf/use-callback + (mf/deps index url on-remove-plugin) + (fn [] + (when on-remove-plugin + (on-remove-plugin index url))))] + [:div {:class (stl/css :plugins-list-element)} + [:div {:class (stl/css :plugin-icon)} ""] + [:div {:class (stl/css :plugin-description)} + [:div {:class (stl/css :plugin-title)} name] + [:div {:class (stl/css :plugin-summary)} description]] + [:button {:class (stl/css :open-button) + :on-click handle-open-click} (tr "workspace.plugins.button-open")] + [: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! + [url] + (.ɵloadPlugin js/window #js {:manifest url})) + +(mf/defc plugin-management-dialog + {::mf/register modal/components + ::mf/register-as :plugin-management} + [] + + (let [plugins-state* (mf/use-state []) + plugins-state @plugins-state* + + plugin-url* (mf/use-state "") + plugin-url @plugin-url* + + input-status* (mf/use-state nil) ;; :error-url :error-manifest :success + input-status @input-status* + + error? (contains? #{:error-url :error-manifest} input-status) + + handle-close-dialog + (mf/use-callback + (fn [] + (modal/hide!))) + + handle-url-input + (mf/use-callback + (fn [value] + (reset! input-status* nil) + (reset! plugin-url* value))) + + handle-install-click + (mf/use-callback + (mf/deps plugins-state plugin-url) + (fn [] + (->> (http/send! {:method :get + :uri plugin-url + :response-type :json}) + (rx/map :body) + (rx/subs! + (fn [body] + (let [name (obj/get body "name") + new-state (conj plugins-state {:name name :url plugin-url})] + (reset! input-status* :success) + (reset! plugin-url* "") + (reset! plugins-state* new-state) + (save-to-store new-state))) + (fn [_] + (reset! input-status* :error-url)))))) + + handle-open-plugin + (mf/use-callback + (fn [_ url] + (open-plugin! url) + (modal/hide!))) + + handle-remove-plugin + (mf/use-callback + (mf/deps plugins-state) + (fn [rm-idx _] + (let [new-state + (into [] + (keep-indexed (fn [idx item] + (when (not= idx rm-idx) item))) + plugins-state)] + + (reset! plugins-state* new-state) + (save-to-store new-state))))] + + (mf/use-effect + (fn [] + (reset! plugins-state* (d/nilv (load-from-store) [])))) + + [:div {:class (stl/css :modal-overlay)} + [:div {:class (stl/css :modal-dialog)} + [:button {:class (stl/css :close-btn) :on-click handle-close-dialog} close-icon] + [:div {:class (stl/css :modal-title)} (tr "workspace.plugins.title")] + + [:div {:class (stl/css :modal-content)} + [:div {:class (stl/css :top-bar)} + [:& search-bar {:on-change handle-url-input + :value plugin-url + :placeholder (tr "workspace.plugins.search-placeholder") + :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? + [:div {:class (stl/css-case :info true :error error?)} + (tr "workspace.plugins.error.url")]) + + [:hr] + + [:& title-bar {:collapsable false + :title (tr "workspace.plugins.installed-plugins")}] + + (if (empty? plugins-state) + [:div {:class (stl/css :plugins-empty)} + [:div {:class (stl/css :plugins-empty-logo)} i/logo-icon] + [:div {:class (stl/css :plugins-empty-text)} (tr "workspace.plugins.empty-plugins")]] + + [:div {:class (stl/css :plugins-list)} + + (for [[idx {:keys [name url]}] (d/enumerate plugins-state)] + [:& plugin-entry {:key (dm/str "plugin-" idx) + :name name + :url url + :index idx + :icon nil + :description "Nullam ullamcorper ligula ac felis commodo pulvinar." + :on-open-plugin handle-open-plugin + :on-remove-plugin handle-remove-plugin}])])]]])) diff --git a/frontend/src/app/main/ui/workspace/plugins.scss b/frontend/src/app/main/ui/workspace/plugins.scss new file mode 100644 index 000000000..583c2d8fc --- /dev/null +++ b/frontend/src/app/main/ui/workspace/plugins.scss @@ -0,0 +1,167 @@ +// 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 + +@import "refactor/common-refactor.scss"; + +.modal-overlay { + @extend .modal-overlay-base; +} + +.modal-dialog { + @extend .modal-container-base; + display: grid; + grid-template-rows: auto 1fr; + height: $s-472; + max-height: $s-472; + width: $s-472; + max-width: $s-472; +} + +.close-btn { + @extend .modal-close-btn-base; +} + +.close-icon { + @extend .button-icon; + stroke: var(--icon-foreground); +} + +.modal-title { + @include headlineMediumTypography; + margin-block-end: $s-16; + color: var(--modal-title-foreground-color); +} + +.modal-content { + display: flex; + flex-direction: column; + height: $s-380; +} + +.primary-button { + @extend .button-primary; + @include headlineSmallTypography; + padding: $s-0 $s-16; +} + +.search-icon { + @include flexCenter; + width: $s-20; + padding: 0 0 0 $s-8; + svg { + @extend .button-icon-small; + stroke: var(--icon-foreground); + } +} + +.top-bar { + display: flex; + gap: $s-8; +} + +.open-button { + @extend .button-secondary; + width: $s-68; + min-width: $s-68; + height: $s-32; + text-transform: uppercase; +} + +.trash-button { + @extend .button-tertiary; + width: $s-32; + height: $s-32; + + svg { + @extend .button-icon; + stroke: var(--icon-foreground); + } +} + +.plugins-list { + padding-top: $s-20; + overflow-x: hidden; + overflow-y: scroll; + flex: 1; + display: flex; + flex-direction: column; + gap: 12px; +} + +.plugins-list-element { + display: flex; + gap: $s-12; +} + +.plugin-icon { + min-width: $s-32; + min-height: $s-32; + width: $s-32; + height: $s-32; + background: #b1b2b5; +} + +.plugin-description { + display: flex; + flex-direction: column; + gap: $s-8; +} + +.plugin-title { + @include bodyMediumTypography; + color: #ffffff; +} + +.plugin-summary { + @include bodySmallTypography; + color: #8f9da3; +} + +.plugins-empty { + display: flex; + flex-direction: column; + align-items: center; + gap: 20px; + margin-top: 3rem; +} + +.plugins-empty-logo { + width: 44px; + height: 44px; + border-radius: 50%; + display: flex; + justify-content: center; + align-items: center; + background: #212426; + + svg { + width: 16px; + height: 16px; + fill: #8f9da3; + } +} + +.plugins-empty-text { + @include bodySmallTypography; + color: white; +} + +div.input-error { + border: 1px solid var(--input-border-color-error); +} + +.info { + @include bodySmallTypography; + margin-top: $s-4; + + &.error { + color: var(--input-border-color-error); + } + + &.success { + color: var(--input-border-color-success); + } +} diff --git a/frontend/src/app/plugins/api.cljs b/frontend/src/app/plugins/api.cljs index 38bda54f0..2476f3ca3 100644 --- a/frontend/src/app/plugins/api.cljs +++ b/frontend/src/app/plugins/api.cljs @@ -20,6 +20,7 @@ [app.main.store :as st] [app.plugins.events :as events] [app.plugins.file :as file] + [app.plugins.library :as library] [app.plugins.page :as page] [app.plugins.shape :as shape] [app.plugins.utils :as utils] @@ -147,11 +148,12 @@ (createShapeFromSvg [_ svg-string] - (let [id (uuid/next) - page-id (:current-page-id @st/state)] - (st/emit! (dwm/create-svg-shape id "svg" svg-string (gpt/point 0 0))) - (shape/data->shape-proxy - (dm/get-in @st/state [:workspace-data :pages-index page-id :objects id]))))) + (when (some? svg-string) + (let [id (uuid/next) + page-id (:current-page-id @st/state)] + (st/emit! (dwm/create-svg-shape id "svg" svg-string (gpt/point 0 0))) + (shape/data->shape-proxy + (dm/get-in @st/state [:workspace-data :pages-index page-id :objects id])))))) (defn create-context [] @@ -160,4 +162,5 @@ {:name "root" :get #(.getRoot ^js %)} {:name "currentPage" :get #(.getPage ^js %)} {:name "selection" :get #(.getSelectedShapes ^js %)} - {:name "viewport" :get #(.getViewport ^js %)})) + {:name "viewport" :get #(.getViewport ^js %)} + {:name "library" :get (fn [_] (library/create-library-subcontext))})) diff --git a/frontend/src/app/plugins/library.cljs b/frontend/src/app/plugins/library.cljs new file mode 100644 index 000000000..cefed442c --- /dev/null +++ b/frontend/src/app/plugins/library.cljs @@ -0,0 +1,96 @@ +;; 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.plugins.library + "RPC for plugins runtime." + (:require + [app.common.data.macros :as dm] + [app.common.record :as cr] + [app.main.store :as st] + [app.plugins.utils :as utils :refer [get-data]] + [app.util.object :as obj])) + +(defn get-library-info + ([self attr] + (let [lib-id (get-data self :id) + current-file-id (:current-file-id @st/state)] + (if (= lib-id current-file-id) + (dm/get-in @st/state [:workspace-file attr]) + (dm/get-in @st/state [:workspace-libraries lib-id attr])))) + + ([self attr mapfn] + (-> (get-library-info self attr) + (mapfn)))) + +(defn get-library-data + ([self attr] + (let [lib-id (get-data self :id) + current-file-id (:current-file-id @st/state)] + (if (= lib-id current-file-id) + (dm/get-in @st/state [:workspace-data attr]) + (dm/get-in @st/state [:workspace-libraries lib-id :data attr])))) + + ([self attr mapfn] + (-> (get-library-data self attr) + (mapfn)))) + +(defn- array-to-js + [value] + (.freeze + js/Object + (apply array (->> value (map utils/to-js))))) + +(deftype Library [_data] + Object) + +(defn create-library + [data] + (cr/add-properties! + (Library. data) + {:name "_data" + :enumerable false} + + {:name "id" + :get (fn [self] + (str (:id (obj/get self "_data"))))} + + {:name "name" + :get (fn [self] + (get-library-info self :name))} + + {:name "colors" + :get (fn [self] + (array-to-js (get-library-data self :colors vals)))} + + {:name "typographies" + :get (fn [self] + (array-to-js (get-library-data self :typographies vals)))} + + {:name "components" + :get (fn [self] + (array-to-js (get-library-data self :components vals)))})) + +(deftype PenpotLibrarySubcontext [] + Object + (find + [_ _name]) + + (find [_])) + +(defn create-library-subcontext + [] + (cr/add-properties! + (PenpotLibrarySubcontext.) + {:name "local" :get + (fn [_] + (let [file (get @st/state :workspace-file) + data (get @st/state :workspace-data)] + (create-library (assoc file :data data))))} + + {:name "connected" :get + (fn [_] + (let [libraries (get @st/state :workspace-libraries)] + (apply array (->> libraries vals (map create-library)))))})) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 1dffa2c85..f83ab5880 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -5175,3 +5175,33 @@ msgstr "Update" msgid "workspace.viewport.click-to-close-path" msgstr "Click to close the path" + +msgid "workspace.plugins.title" +msgstr "Plugins" + +msgid "workspace.plugins.search-placeholder" +msgstr "Write a plugin URL" + +msgid "workspace.plugins.install" +msgstr "Install" + +msgid "workspace.plugins.installed-plugins" +msgstr "Installed plugins" + +msgid "workspace.plugins.empty-plugins" +msgstr "No plugins installed yet" + +msgid "workspace.plugins.button-open" +msgstr "Open" + +msgid "workspace.plugins.error.url" +msgstr "The plugin doesn't exist or the URL is not correct." + +msgid "workspace.plugins.success" +msgstr "Plugin correctly loaded." + +msgid "workspace.plugins.menu.title" +msgstr "Plugins" + +msgid "workspace.plugins.menu.plugins-manager" +msgstr "Plugins manager" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 46b7dd8fc..d0ad70298 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -5301,3 +5301,34 @@ msgstr "Actualizar" msgid "workspace.viewport.click-to-close-path" msgstr "Pulsar para cerrar la ruta" + +msgid "workspace.plugins.title" +msgstr "Extensiones" + +msgid "workspace.plugins.search-placeholder" +msgstr "Intruduzca URL de la extensión" + +msgid "workspace.plugins.install" +msgstr "Instalar" + +msgid "workspace.plugins.installed-plugins" +msgstr "Extensiones instaladas" + +msgid "workspace.plugins.empty-plugins" +msgstr "No se encuentran extensiones" + +msgid "workspace.plugins.button-open" +msgstr "Abrir" + +msgid "workspace.plugins.error.url" +msgstr "La extensión no existe o la url no es correcta." + +msgid "workspace.plugins.success" +msgstr "Extensión cargada correctamente." + +msgid "workspace.plugins.menu.title" +msgstr "Extensiones" + +msgid "workspace.plugins.menu.plugins-manager" +msgstr "Gestor de extensiones" +