diff --git a/CHANGES.md b/CHANGES.md index 0c88e78c9..e232c9f3e 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -8,6 +8,7 @@ - Add cosmetic changes in viewer mode [Taiga #3688](https://tree.taiga.io/project/penpot/us/3688) - Outline highlights on layer hovering [Taiga #2645](https://tree.taiga.io/project/penpot/us/2645) by @andrewzhurov - Add zoom to shape on double click up on its icon [Taiga #3929](https://tree.taiga.io/project/penpot/us/3929) by @andrewzhurov +- Add Libraries & Templates carousel [Taiga #3860](https://tree.taiga.io/project/penpot/us/3860) ### :bug: Bugs fixed diff --git a/backend/src/app/rpc/commands/management.clj b/backend/src/app/rpc/commands/management.clj index 4e43d7961..fa78a6299 100644 --- a/backend/src/app/rpc/commands/management.clj +++ b/backend/src/app/rpc/commands/management.clj @@ -400,4 +400,4 @@ (sv/defmethod ::retrieve-list-of-builtin-templates [cfg _params] - (mapv #(select-keys % [:id :name]) (:templates cfg))) + (mapv #(select-keys % [:id :name :thumbnail-uri]) (:templates cfg))) diff --git a/frontend/resources/images/icons/arrow-up.svg b/frontend/resources/images/icons/arrow-up.svg new file mode 100644 index 000000000..63c47d9c8 --- /dev/null +++ b/frontend/resources/images/icons/arrow-up.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/frontend/resources/styles/main/layouts/main-layout.scss b/frontend/resources/styles/main/layouts/main-layout.scss index cdcd7f22b..ced60856b 100644 --- a/frontend/resources/styles/main/layouts/main-layout.scss +++ b/frontend/resources/styles/main/layouts/main-layout.scss @@ -33,6 +33,7 @@ .dashboard-content { display: flex; flex-direction: column; + position: relative; } .verify-token { diff --git a/frontend/resources/styles/main/partials/dashboard-sidebar.scss b/frontend/resources/styles/main/partials/dashboard-sidebar.scss index fa571cd84..818ad0ae2 100644 --- a/frontend/resources/styles/main/partials/dashboard-sidebar.scss +++ b/frontend/resources/styles/main/partials/dashboard-sidebar.scss @@ -6,6 +6,7 @@ .dashboard-sidebar { background-color: $color-white; + z-index: 1; .sidebar-inside { display: flex; diff --git a/frontend/resources/styles/main/partials/dashboard.scss b/frontend/resources/styles/main/partials/dashboard.scss index eb7e9e45e..6f5580c2b 100644 --- a/frontend/resources/styles/main/partials/dashboard.scss +++ b/frontend/resources/styles/main/partials/dashboard.scss @@ -254,3 +254,155 @@ height: 16px; } } + +.dashboard-templates-section { + position: absolute; + bottom: 0; + width: 100%; + height: 285px; + transition: bottom 300ms; + + &.collapsed { + bottom: -228px; + transition: bottom 300ms; + } + + .title { + width: 100%; + text-align: right; + height: 56px; + cursor: pointer; + div { + height: 58px; + display: inline-block; + line-height: 58px; + text-align: center; + border-top: 2px solid #e4e4e4; + border-left: 2px solid #e4e4e4; + border-right: 2px solid #e4e4e4; + border-top-left-radius: 10px; + border-top-right-radius: 10px; + margin-right: 30px; + background-color: $color-white; + position: relative; + z-index: 1; + span { + display: inline-block; + vertical-align: middle; + line-height: normal; + font-size: 18px; + font-weight: 600; + color: $color-black; + margin-left: 20px; + margin-right: 10px; + } + svg { + width: 12px; + height: 12px; + } + } + } + + .button { + position: absolute; + top: 133px; + border: 2px solid #e0e4e9; + border-radius: 50%; + text-align: center; + line-height: 35px; + width: 35px; + height: 35px; + cursor: pointer; + background-color: $color-white; + + svg { + width: 12px; + height: 12px; + } + + &.left { + left: 0; + margin-left: 43px; + } + + &.right { + right: 0; + margin-right: 43px; + } + + &:hover { + border: 2px solid $color-primary; + } + } + + .content { + background-color: $color-white; + width: 200%; + height: 229px; + border-top: 2px solid #e4e4e4; + border-left: 2px solid #e4e4e4; + margin-left: 5px; + position: absolute; + + .card-container { + width: 275px; + height: 100%; + margin-top: 20px; + display: inline-block; + text-align: center; + vertical-align: top; + } + + .template-card { + display: inline-block; + width: 255px; + font-size: 16px; + color: #181a22; + cursor: pointer; + .img-container { + width: 100%; + height: 135px; + margin-bottom: 15px; + border-radius: 5px; + border: 2px solid #e0e4e9; + display: flex; + justify-content: center; + flex-direction: column; + } + + .card-name { + padding: 0 5px; + display: flex; + justify-content: space-between; + height: 23px; + svg { + width: 16px; + height: 16px; + } + } + + .template-link { + border: 2px solid transparent; + margin: 30px; + padding: 32px 0; + } + + .template-link-title { + font-size: 14px; + font-weight: 600; + color: $color-gray-60; + } + + .template-link-text { + font-size: 12px; + color: $color-gray-30; + } + + &:hover { + .img-container { + border: 2px solid $color-primary; + } + } + } + } +} diff --git a/frontend/src/app/main/data/dashboard.cljs b/frontend/src/app/main/data/dashboard.cljs index 071cb4b3f..c2d7b84c8 100644 --- a/frontend/src/app/main/data/dashboard.cljs +++ b/frontend/src/app/main/data/dashboard.cljs @@ -60,6 +60,7 @@ (declare fetch-projects) (declare fetch-team-members) +(declare fetch-builtin-templates) (defn initialize [{:keys [id] :as params}] @@ -87,7 +88,8 @@ (ptk/watch (fetch-projects) state stream) (ptk/watch (fetch-team-members) state stream) (ptk/watch (du/fetch-teams) state stream) - (ptk/watch (du/fetch-users {:team-id id}) state stream))))) + (ptk/watch (du/fetch-users {:team-id id}) state stream) + (ptk/watch (fetch-builtin-templates) state stream))))) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Fetching (context aware: current team) @@ -293,6 +295,24 @@ (->> (rp/query :team-recent-files {:team-id team-id}) (rx/map recent-files-fetched))))))) + +;; --- EVENT: fetch-team-invitations + +(defn builtin-templates-fetched + [libraries] + (ptk/reify ::libraries-fetched + ptk/UpdateEvent + (update [_ state] + (assoc state :builtin-templates libraries)))) + +(defn fetch-builtin-templates + [] + (ptk/reify ::fetch-builtin-templates + ptk/WatchEvent + (watch [_ _ _] + (->> (rp/command :retrieve-list-of-builtin-templates) + (rx/map builtin-templates-fetched))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Data Selection ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -795,6 +815,27 @@ (rx/tap on-success) (rx/catch on-error)))))) + +;; --- EVENT: clone-template +(defn clone-template + [{:keys [template-id project-id] :as params}] + (us/assert ::us/uuid project-id) + (ptk/reify ::clone-template + IDeref + (-deref [_] + {:template-id template-id + :project-id project-id}) + + ptk/WatchEvent + (watch [_ _ _] + (let [{:keys [on-success on-error] + :or {on-success identity + on-error rx/throw}} (meta params)] + + (->> (rp/command! :clone-template {:project-id project-id :template-id template-id}) + (rx/tap on-success) + (rx/catch on-error)))))) + ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; ;; Navigation ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; diff --git a/frontend/src/app/main/data/events.cljs b/frontend/src/app/main/data/events.cljs index b457cd8bf..4c85decb8 100644 --- a/frontend/src/app/main/data/events.cljs +++ b/frontend/src/app/main/data/events.cljs @@ -95,6 +95,7 @@ (derive :app.main.data.dashboard/set-file-shared ::generic-action) (derive :app.main.data.dashboard/update-team-member-role ::generic-action) (derive :app.main.data.dashboard/update-team-photo ::generic-action) +(derive :app.main.data.dashboard/clone-template ::generic-action) (derive :app.main.data.fonts/add-font ::generic-action) (derive :app.main.data.fonts/delete-font ::generic-action) (derive :app.main.data.fonts/delete-font-variant ::generic-action) diff --git a/frontend/src/app/main/ui/dashboard.cljs b/frontend/src/app/main/ui/dashboard.cljs index 0f16ffaa8..158b753a6 100644 --- a/frontend/src/app/main/ui/dashboard.cljs +++ b/frontend/src/app/main/ui/dashboard.cljs @@ -7,9 +7,13 @@ (ns app.main.ui.dashboard (:require [app.common.colors :as clr] + [app.common.data :as d] [app.common.spec :as us] [app.main.data.dashboard :as dd] [app.main.data.dashboard.shortcuts :as sc] + [app.main.data.events :as ev] + [app.main.data.modal :as modal] + [app.main.data.users :as du] [app.main.refs :as refs] [app.main.store :as st] [app.main.ui.context :as ctx] @@ -23,9 +27,16 @@ [app.main.ui.dashboard.sidebar :refer [sidebar]] [app.main.ui.dashboard.team :refer [team-settings-page team-members-page team-invitations-page]] [app.main.ui.hooks :as hooks] + [app.main.ui.icons :as i] [app.util.dom :as dom] + [app.util.i18n :refer [tr]] [app.util.keyboard :as kbd] + [app.util.object :as obj] + [app.util.router :as rt] + [cuerdas.core :as str] [goog.events :as events] + [okulary.core :as l] + [potok.core :as ptk] [rumext.alpha :as mf]) (:import goog.events.EventType)) @@ -48,40 +59,180 @@ (uuid-str? project-id) (assoc :project-id (uuid project-id))))) +(def builtin-templates + (l/derived :builtin-templates st/state)) + +(mf/defc templates-section + [{:keys [default-project-id profile project team content-width] :as props}] + (let [templates (mf/deref builtin-templates) + route (mf/deref refs/route) + route-name (get-in route [:data :name]) + section (if (= route-name :dashboard-files) + (if (= (:id project) default-project-id) + "dashboard-drafts" + "dashboard-project") + (name route-name)) + props (some-> profile (get :props {})) + collapsed (:builtin-templates-collapsed-status props false) + card-offset (mf/use-state 0) + + card-width 275 + num-cards (count templates) + ;; We need space for num-cards plus the libraries&templates link + more-cards (> (+ @card-offset (* (+ 1 num-cards) card-width)) content-width) + content-ref (mf/use-ref) + + toggle-collapse + (fn [] + (st/emit! + (du/update-profile-props {:builtin-templates-collapsed-status (not collapsed)}))) + + move-left + (fn [] + (when-not (zero? @card-offset) + (dom/animate! (mf/ref-val content-ref) + [#js {:left (str @card-offset "px")} + #js {:left (str (+ @card-offset card-width) "px")}] + #js {:duration 200 + :easing "linear"}) + (reset! card-offset (+ @card-offset card-width)))) + + move-right + (fn [] + (when more-cards (swap! card-offset inc) + (dom/animate! (mf/ref-val content-ref) + [#js {:left (str @card-offset "px")} + #js {:left (str (- @card-offset card-width) "px")}] + #js {:duration 200 + :easing "linear"}) + (reset! card-offset (- @card-offset card-width)))) + + on-finish-import + (fn [template] + (st/emit! + (ptk/event ::ev/event {::ev/name "import-template-finish" + ::ev/origin "dashboard" + :template (:name template) + :section section}) + (when (not (some? project)) (rt/nav :dashboard-files + {:team-id (:id team) + :project-id default-project-id})))) + + import-template + (fn [template] + (let [templates-project-id (if project (:id project) default-project-id)] + (st/emit! + (ptk/event ::ev/event {::ev/name "import-template-launch" + ::ev/origin "dashboard" + :template (:name template) + :section section}) + + (modal/show + {:type :import + :project-id templates-project-id + :files [] + :template template + :on-finish-import (partial on-finish-import template)})))) + + handle-template-link + (fn [] + (st/emit! (ptk/event ::ev/event {::ev/name "explore-libraries-click" + ::ev/origin "dashboard" + :section section})))] + + [:div.dashboard-templates-section {:class (when collapsed "collapsed")} + [:div.title {:on-click toggle-collapse} + [:div + [:span (tr "dashboard.libraries-and-templates")] + [:span.icon (if collapsed i/arrow-up i/arrow-down)]]] + [:div.content {:ref content-ref + :style {:left @card-offset}} + (for [num-item (range (count templates)) :let [item (nth templates num-item)]] + [:div.card-container {:id (str/concat "card-container-" num-item) + :key (:id item) + :on-click #(import-template item)} + [:div.template-card + [:div.img-container + [:img {:src (:thumbnail-uri item)}]] + [:div.card-name [:span (:name item)] [:span.icon i/download]]]]) + + [:div.card-container + [:div.template-card + [:div.img-container + [:a {:href "https://penpot.app/libraries-templates.html" :target "_blank" :on-click handle-template-link} + [:div.template-link + [:div.template-link-title (tr "dashboard.libraries-and-templates")] + [:div.template-link-text (tr "dashboard.libraries-and-templates.explore")]]]]]]] + (when (< @card-offset 0) + [:div.button.left {:on-click move-left} i/go-prev]) + (when more-cards + [:div.button.right {:on-click move-right} i/go-next])])) + (mf/defc dashboard-content [{:keys [team projects project section search-term profile] :as props}] - [:div.dashboard-content {:on-click #(st/emit! (dd/clear-selected-files))} - (case section - :dashboard-projects - [:& projects-section {:team team :projects projects}] + (let [container (mf/use-ref) + content-width (mf/use-state 0) + default-project-id + (->> (vals projects) + (d/seek :is-default) + (:id)) + on-resize + (fn [_] + (let [dom (mf/ref-val container) + width (obj/get dom "clientWidth")] + (reset! content-width width)))] - :dashboard-fonts - [:& fonts-page {:team team}] + (mf/use-effect + #(let [key1 (events/listen js/window "resize" on-resize)] + (fn [] + (events/unlistenByKey key1)))) - :dashboard-font-providers - [:& font-providers-page {:team team}] + (mf/use-effect on-resize) + [:div.dashboard-content {:on-click #(st/emit! (dd/clear-selected-files)) :ref container} + (case section + :dashboard-projects + [:* + [:& projects-section {:team team :projects projects}] + [:& templates-section {:profile profile + :project project + :default-project-id default-project-id + :team team + :content-width @content-width}]] - :dashboard-files - (when project - [:& files-section {:team team :project project}]) + :dashboard-fonts + [:& fonts-page {:team team}] - :dashboard-search - [:& search-page {:team team - :search-term search-term}] + :dashboard-font-providers + [:& font-providers-page {:team team}] - :dashboard-libraries - [:& libraries-page {:team team}] + :dashboard-files + (when project + [:* + [:& files-section {:team team :project project}] + [:& templates-section {:profile profile + :project project + :default-project-id default-project-id + :team team + :content-width @content-width}]]) - :dashboard-team-members - [:& team-members-page {:team team :profile profile}] + :dashboard-search + [:& search-page {:team team + :search-term search-term}] - :dashboard-team-invitations - [:& team-invitations-page {:team team}] + :dashboard-libraries + [* + [:& libraries-page {:team team}]] - :dashboard-team-settings - [:& team-settings-page {:team team :profile profile}] + :dashboard-team-members + [:& team-members-page {:team team :profile profile}] - nil)]) + :dashboard-team-invitations + [:& team-invitations-page {:team team}] + + :dashboard-team-settings + [:& team-settings-page {:team team :profile profile}] + + nil)])) (mf/defc dashboard [{:keys [route profile] :as props}] diff --git a/frontend/src/app/main/ui/dashboard/files.cljs b/frontend/src/app/main/ui/dashboard/files.cljs index d7e4a2d00..9960c5e0f 100644 --- a/frontend/src/app/main/ui/dashboard/files.cljs +++ b/frontend/src/app/main/ui/dashboard/files.cljs @@ -118,7 +118,7 @@ (dom/prevent-default event) (st/emit! (with-meta (dd/create-file {:project-id (:id project)}) {::ev/origin origin}))))] - + (mf/use-effect (fn [] (let [node (mf/ref-val rowref) @@ -134,7 +134,7 @@ (fn [] (vreset! mnt? false) (rx/dispose! sub))))) - + (mf/use-effect (mf/deps project) @@ -152,7 +152,7 @@ (dd/clear-selected-files)))) [:* - [:& header {:team team :project project + [:& header {:team team :project project :on-create-clicked on-create-clicked}] [:section.dashboard-container.no-bg {:ref rowref} [:& grid {:project project diff --git a/frontend/src/app/main/ui/dashboard/import.cljs b/frontend/src/app/main/ui/dashboard/import.cljs index 93b48d987..77cb945d2 100644 --- a/frontend/src/app/main/ui/dashboard/import.cljs +++ b/frontend/src/app/main/ui/dashboard/import.cljs @@ -8,7 +8,9 @@ (:require [app.common.data :as d] [app.common.logging :as log] + [app.main.data.dashboard :as dd] [app.main.data.events :as ev] + [app.main.data.messages :as dm] [app.main.data.modal :as modal] [app.main.store :as st] [app.main.ui.components.file-uploader :refer [file-uploader]] @@ -236,10 +238,11 @@ (mf/defc import-dialog {::mf/register modal/components ::mf/register-as :import} - [{:keys [project-id files on-finish-import]}] + [{:keys [project-id files template on-finish-import]}] (let [state (mf/use-state {:status :analyzing :editing nil + :importing-templates 0 :files (->> files (mapv #(assoc % :status :analyzing)))}) @@ -278,19 +281,50 @@ (dom/prevent-default event) (st/emit! (modal/hide))))) + on-template-cloned-success + (fn [] + (swap! state + (fn [state] + (-> state + (assoc :status :importing :importing-templates 0)))) + (st/emit! (dd/fetch-recent-files))) + + on-template-cloned-error + (fn [] + (st/emit! + (modal/hide) + (dm/error (tr "dashboard.libraries-and-templates.import-error")))) + + continue-files + (fn [] + (let [files (->> @state :files (filterv #(and (= :ready (:status %)) (not (:deleted? %)))))] + (import-files project-id files)) + + (swap! state + (fn [state] + (-> state + (assoc :status :importing) + (update :files mark-files-importing))))) + + continue-template + (fn [] + (let [mdata {:on-success on-template-cloned-success :on-error on-template-cloned-error} + params {:project-id project-id :template-id (:id template)}] + (swap! state + (fn [state] + (-> state + (assoc :status :importing :importing-templates 1)))) + (st/emit! (dd/clone-template (with-meta params mdata))))) + + handle-continue (mf/use-callback (mf/deps project-id (:files @state)) (fn [event] (dom/prevent-default event) - (let [files (->> @state :files (filterv #(and (= :ready (:status %)) (not (:deleted? %)))))] - (import-files project-id files)) - - (swap! state - (fn [state] - (-> state - (assoc :status :importing) - (update :files mark-files-importing)))))) + (if (some? template) + (continue-template) + (continue-files)))) handle-accept (mf/use-callback @@ -299,10 +333,15 @@ (st/emit! (modal/hide)) (when on-finish-import (on-finish-import)))) + num-importing (+ + (->> @state :files (filter #(= (:status %) :importing)) count) + (:importing-templates @state)) + + warning-files (->> @state :files (filter #(and (= (:status %) :import-finish) (d/not-empty? (:errors %)))) count) success-files (->> @state :files (filter #(and (= (:status %) :import-finish) (empty? (:errors %)))) count) pending-analysis? (> (->> @state :files (filter #(= (:status %) :analyzing)) count) 0) - pending-import? (> (->> @state :files (filter #(= (:status %) :importing)) count) 0) + pending-import? (> num-importing 0) files (->> (:files @state) (filterv (comp not :deleted?)))] (mf/use-effect @@ -334,7 +373,7 @@ [:div.feedback-banner [:div.icon i/checkbox-checked] - [:div.message (tr "dashboard.import.import-message" success-files)]])) + [:div.message (tr "dashboard.import.import-message" (if (some? template) 1 success-files))]])) (for [file files] (let [editing? (and (some? (:file-id file)) @@ -342,7 +381,13 @@ [:& import-entry {:state state :file file :editing? editing? - :can-be-deleted? (> (count files) 1)}]))] + :can-be-deleted? (> (count files) 1)}])) + + (when (some? template) + [:& import-entry {:state state + :file (assoc template :status (if (= 1 (:importing-templates @state)) :importing :ready)) + :editing? false + :can-be-deleted? false}])] [:div.modal-footer [:div.action-buttons diff --git a/frontend/src/app/main/ui/dashboard/sidebar.cljs b/frontend/src/app/main/ui/dashboard/sidebar.cljs index a6b1def0f..ed648690d 100644 --- a/frontend/src/app/main/ui/dashboard/sidebar.cljs +++ b/frontend/src/app/main/ui/dashboard/sidebar.cljs @@ -172,7 +172,7 @@ (dom/clean-value! search-input) (dom/focus! search-input) (emit! (dd/go-to-search))))) - + on-key-press (mf/use-callback (fn [e] diff --git a/frontend/src/app/main/ui/icons.cljs b/frontend/src/app/main/ui/icons.cljs index ae5d38542..932cdc506 100644 --- a/frontend/src/app/main/ui/icons.cljs +++ b/frontend/src/app/main/ui/icons.cljs @@ -24,6 +24,7 @@ (def arrow-down (icon-xref :arrow-down)) (def arrow-end (icon-xref :arrow-end)) (def arrow-slide (icon-xref :arrow-slide)) +(def arrow-up (icon-xref :arrow-up)) (def artboard (icon-xref :artboard)) (def at (icon-xref :at)) (def auto-direction (icon-xref :auto-direction)) diff --git a/frontend/src/app/main/ui/onboarding.cljs b/frontend/src/app/main/ui/onboarding.cljs index 4e9f8a952..0ecd60ac1 100644 --- a/frontend/src/app/main/ui/onboarding.cljs +++ b/frontend/src/app/main/ui/onboarding.cljs @@ -9,7 +9,7 @@ [app.config :as cf] [app.main.data.events :as ev] [app.main.data.modal :as modal] - [app.main.data.users :as du] + [app.main.data.users :as du] [app.main.store :as st] [app.main.ui.onboarding.newsletter] [app.main.ui.onboarding.questions] @@ -22,7 +22,7 @@ ;; --- ONBOARDING LIGHTBOX -(defn send-event +(defn send-event [event-name] (st/emit! (ptk/event ::ev/event {::ev/name event-name ::ev/origin "dashboard"}))) diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 961f46910..4dacaa954 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -643,6 +643,15 @@ msgstr "Your name" msgid "dashboard.your-penpot" msgstr "Your Penpot" +msgid "dashboard.libraries-and-templates" +msgstr "Libraries & Templates" + +msgid "dashboard.libraries-and-templates.explore" +msgstr "Explore more of them and know how to contribute" + +msgid "dashboard.libraries-and-templates.import-error" +msgstr "There was a problem importing the template. The template wasn't imported." + #: src/app/main/ui/alert.cljs msgid "ds.alert-ok" msgstr "Ok" diff --git a/frontend/translations/es.po b/frontend/translations/es.po index 57023a58c..74ca0d36d 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -663,6 +663,15 @@ msgstr "Tu nombre" msgid "dashboard.your-penpot" msgstr "Tu Penpot" +msgid "dashboard.libraries-and-templates" +msgstr "Bibliotecas y plantillas" + +msgid "dashboard.libraries-and-templates.explore" +msgstr "Explora más y descubre cómo contribuir" + +msgid "dashboard.libraries-and-templates.import-error" +msgstr "Hubo un problema importando la plantilla. No ha podido ser importada." + #: src/app/main/ui/alert.cljs msgid "ds.alert-ok" msgstr "Ok"