Add organization selection for nitrate (#8619)

This commit is contained in:
Pablo Alba 2026-03-13 09:16:12 +01:00 committed by GitHub
parent 3eabbffb0e
commit 08845ad2d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 271 additions and 99 deletions

View File

@ -82,8 +82,9 @@
(def ^:private schema:organization (def ^:private schema:organization
[:map [:map
[:id ::sm/text] [:id ::sm/uuid]
[:name ::sm/text]]) [:name ::sm/text]
[:slug ::sm/text]])
;; TODO Unify with schemas on backend/src/app/http/management.clj ;; TODO Unify with schemas on backend/src/app/http/management.clj
(def ^:private schema:timestamp (def ^:private schema:timestamp
@ -189,6 +190,9 @@
(defn add-nitrate-licence-to-profile (defn add-nitrate-licence-to-profile
"Enriches a profile map with subscription information from Nitrate.
Adds a :subscription field containing the user's license details.
Returns the original profile unchanged if the request fails."
[cfg profile] [cfg profile]
(try (try
(let [subscription (call cfg :get-subscription {:profile-id (:id profile)})] (let [subscription (call cfg :get-subscription {:profile-id (:id profile)})]
@ -199,11 +203,26 @@
:cause cause) :cause cause)
profile))) profile)))
(defn add-org-to-team (defn add-org-info-to-team
"Enriches a team map with organization information from Nitrate.
Adds organization-id, organization-name, organization-slug, and your-penpot fields.
Returns the original team unchanged if the request fails or org data is nil."
[cfg team params] [cfg team params]
(let [params (assoc (or params {}) :team-id (:id team)) (try
org (call cfg :get-team-org params)] (let [params (assoc (or params {}) :team-id (:id team))
(assoc team :organization-id (:id org) :organization-name (:name org)))) org (call cfg :get-team-org params)]
(if (some? org)
(assoc team
:organization-id (:id org)
:organization-name (:name org)
:organization-slug (:slug org)
:is-default (or (:is-default team) (true? (:isYourPenpot org))))
team))
(catch Throwable cause
(l/error :hint "failed to get team organization info"
:team-id (:id team)
:cause cause)
team)))
(defn connectivity (defn connectivity
[cfg] [cfg]

View File

@ -193,7 +193,7 @@
(dm/with-open [conn (db/open pool)] (dm/with-open [conn (db/open pool)]
(cond->> (get-teams conn profile-id) (cond->> (get-teams conn profile-id)
(contains? cf/flags :nitrate) (contains? cf/flags :nitrate)
(map #(nitrate/add-org-to-team cfg % params))))) (map #(nitrate/add-org-info-to-team cfg % params)))))
(def ^:private sql:get-owned-teams (def ^:private sql:get-owned-teams
"SELECT t.id, t.name, "SELECT t.id, t.name,

View File

@ -122,8 +122,7 @@
(set/difference cfeat/no-team-inheritable-features)) (set/difference cfeat/no-team-inheritable-features))
params {:profile-id profile-id params {:profile-id profile-id
:name "Default" :name "Default"
:features features :features features}
:is-default true}
team (db/tx-run! cfg teams/create-team params)] team (db/tx-run! cfg teams/create-team params)]
(select-keys team [:id]))) (select-keys team [:id])))

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" stroke-linecap="round" stroke-linejoin="round">
<path d="M2.667 13.333 13.333 2.667m0 0H2.667m10.666 0v10.666"/>
</svg>

After

Width:  |  Height:  |  Size: 184 B

View File

@ -17,12 +17,17 @@
(watch [_ _ _] (watch [_ _ _]
(->> (rp/cmd! ::get-nitrate-connectivity {}) (->> (rp/cmd! ::get-nitrate-connectivity {})
(rx/map (fn [connectivity] (rx/map (fn [connectivity]
(prn "connectivity" connectivity)
(modal/show popup-type (or connectivity {})))))))) (modal/show popup-type (or connectivity {}))))))))
(defn go-to-nitrate-cc (defn go-to-nitrate-cc
[] ([]
(st/emit! (rt/nav-raw :href "/control-center/"))) (st/emit! (rt/nav-raw :href "/control-center/")))
([{:keys [organization-id organization-slug]}]
(let [href (dm/str "/control-center/org/"
(u/percent-encode organization-slug)
"/"
(u/percent-encode (str organization-id)))]
(st/emit! (rt/nav-raw :href href)))))
(defn go-to-nitrate-billing (defn go-to-nitrate-billing
[] []

View File

@ -78,6 +78,12 @@
(def ^:private exit-icon (def ^:private exit-icon
(deprecated-icon/icon-xref :exit (stl/css :exit-icon))) (deprecated-icon/icon-xref :exit (stl/css :exit-icon)))
(def ^:private add-org-icon
(deprecated-icon/icon-xref :add (stl/css :add-org-icon)))
(def ^:private arrow-up-right-icon
(deprecated-icon/icon-xref :arrow-up-right (stl/css :arrow-up-right-icon)))
(def ^:private ^:svg-id penpot-logo-icon "penpot-logo-icon") (def ^:private ^:svg-id penpot-logo-icon "penpot-logo-icon")
(mf/defc sidebar-project* (mf/defc sidebar-project*
@ -285,13 +291,10 @@
:on-click on-clear-click} :on-click on-clear-click}
search-icon])])) search-icon])]))
(mf/defc teams-selector-dropdown* (mf/defc organizations-selector-dropdown*
{::mf/private true} {::mf/private true}
[{:keys [team profile teams show-default-team allow-create-teams allow-create-org] :rest props}] [{:keys [organization organizations profile] :rest props}]
(let [on-create-team-click (let [on-org-click
(mf/use-fn #(st/emit! (modal/show :team-form {})))
on-team-click
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(let [team-id (-> (dom/get-current-target event) (let [team-id (-> (dom/get-current-target event)
@ -301,23 +304,93 @@
on-create-org-click on-create-org-click
(mf/use-fn (mf/use-fn
(mf/deps profile)
(fn [] (fn []
(if (dnt/is-valid-license? profile) (if (dnt/is-valid-license? profile)
(dnt/go-to-nitrate-cc) (dnt/go-to-nitrate-cc)
(st/emit! (dnt/show-nitrate-popup :nitrate-form)))))] (st/emit! (dnt/show-nitrate-popup :nitrate-form)))))
on-go-to-cc-click
(mf/use-fn
(mf/deps organization)
(fn []
(dnt/go-to-nitrate-cc organization)))
default-team-id (or (->> organizations
vals
(filter :is-default)
first
:id)
(:default-team-id profile))
organizations (dissoc organizations default-team-id)]
[:> dropdown-menu* props [:> dropdown-menu* props
(when show-default-team [:> dropdown-menu-item* {:on-click on-org-click
[:> dropdown-menu-item* {:on-click on-team-click :data-value default-team-id
:data-value (:default-team-id profile) :class (stl/css :org-dropdown-item)}
:class (stl/css :team-dropdown-item)} [:span {:class (stl/css :nitrate-org-icon)}
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon] [:> raw-svg* {:id penpot-logo-icon}]]
"Penpot"
(when (= default-team-id (:id organization))
tick-icon)]
[:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")] (for [org-item (remove :is-default (vals organizations))]
(when (= (:default-team-id profile) (:id team)) [:> dropdown-menu-item* {:on-click on-org-click
:data-value (:id org-item)
:class (stl/css :org-dropdown-item)
:key (str (:id org-item))}
;; TODO org pictures
[:img {:src (cf/resolve-team-photo-url org-item)
:class (stl/css :team-picture)
:alt (:name org-item)}]
[:span {:class (stl/css :team-text)
:title (:name org-item)} (:name org-item)]
(when (= (:id org-item) (:id organization))
tick-icon)]) tick-icon)])
[:hr {:role "separator" :class (stl/css :team-separator)}]
[:> dropdown-menu-item* {:on-click on-create-org-click
:class (stl/css :org-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} add-org-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-org")]]
[:> dropdown-menu-item* {:on-click on-go-to-cc-click
:class (stl/css :org-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} arrow-up-right-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.go-to-control-center")]]]))
(mf/defc teams-selector-dropdown*
{::mf/private true}
[{:keys [team profile teams] :rest props}]
(let [default-team-id (or (->> teams
vals
(filter :is-default)
first
:id)
(:default-team-id profile))
teams (dissoc teams default-team-id)
on-create-team-click
(mf/use-fn #(st/emit! (modal/show :team-form {})))
on-team-click
(mf/use-fn
(fn [event]
(let [team-id (-> (dom/get-current-target event)
(dom/get-data "value")
(uuid/parse))]
(st/emit! (dcm/go-to-dashboard-recent :team-id team-id)))))]
[:> dropdown-menu* props
[:> dropdown-menu-item* {:on-click on-team-click
:data-value (:default-team-id profile)
:class (stl/css :team-dropdown-item)}
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.your-penpot")]
(when (= (:default-team-id profile) (:id team))
tick-icon)]
(for [team-item (remove :is-default (vals teams))] (for [team-item (remove :is-default (vals teams))]
[:> dropdown-menu-item* {:on-click on-team-click [:> dropdown-menu-item* {:on-click on-team-click
:data-value (:id team-item) :data-value (:id team-item)
@ -337,19 +410,12 @@
(when (= (:id team-item) (:id team)) (when (= (:id team-item) (:id team))
tick-icon)]) tick-icon)])
(when allow-create-teams
[:hr {:role "separator" :class (stl/css :team-separator)}]
[:> dropdown-menu-item* {:on-click on-create-team-click
:class (stl/css :team-dropdown-item :action)}
[:span {:class (stl/css :icon-wrapper)} add-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]])
(when allow-create-org [:hr {:role "separator" :class (stl/css :team-separator)}]
[:hr {:role "separator" :class (stl/css :team-separator)}] [:> dropdown-menu-item* {:on-click on-create-team-click
[:> dropdown-menu-item* {:on-click on-create-org-click :class (stl/css :team-dropdown-item :action)}
:class (stl/css :team-dropdown-item :action)} [:span {:class (stl/css :icon-wrapper)} add-icon]
[:span {:class (stl/css :icon-wrapper)} add-icon] [:span {:class (stl/css :team-text)} (tr "dashboard.create-new-team")]]]))
[:span {:class (stl/css :team-text)} (tr "dashboard.create-new-org")]])]))
(mf/defc team-options-dropdown* (mf/defc team-options-dropdown*
{::mf/private true} {::mf/private true}
@ -500,39 +566,41 @@
(tr "dashboard.delete-team")])])) (tr "dashboard.delete-team")])]))
(defn- team->org [team]
(assoc (dm/select-keys team [:id :organization-id :organization-slug])
:name (:organization-name team)))
(mf/defc sidebar-org-switch* (mf/defc sidebar-org-switch*
[{:keys [team profile]}] [{:keys [team profile]}]
(let [teams (mf/deref refs/teams) (let [teams (mf/deref refs/teams)
;; Find the "your-penpot" teams, and transform them in orgs
orgs (mf/with-memo [teams] orgs (mf/with-memo [teams]
(let [orgs (->> teams (->> teams
vals vals
(group-by :organization-id) (filter :is-default)
(map (fn [[_group entries]] (first entries))) (map team->org)
vec (d/index-by :id)))
(d/index-by :id))]
(update-vals orgs
(fn [t]
(assoc t :name (str "ORG: " (:organization-name t)))))))
empty? (= (count orgs) 1) no-orgs? (= (count orgs) 0)
current-org (team->org team)
current-org (mf/with-memo [team] default-org? (= (:default-team-id profile) (:id current-org))
(assoc team :name (str "ORG: " (:organization-name team))))
show-teams-menu* show-orgs-menu*
(mf/use-state false) (mf/use-state false)
show-teams-menu? show-orgs-menu?
(deref show-teams-menu*) (deref show-orgs-menu*)
on-show-teams-click on-show-orgs-click
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(dom/stop-propagation event) (dom/stop-propagation event)
(swap! show-teams-menu* not))) (swap! show-orgs-menu* not)))
on-show-teams-keydown on-show-orgs-keydown
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
(when (or (kbd/space? event) (when (or (kbd/space? event)
@ -541,51 +609,58 @@
(dom/stop-propagation event) (dom/stop-propagation event)
(some-> (dom/get-current-target event) (some-> (dom/get-current-target event)
(dom/click))))) (dom/click)))))
close-teams-menu close-orgs-menu
(mf/use-fn #(reset! show-teams-menu* false)) (mf/use-fn #(reset! show-orgs-menu* false))
on-create-org-click on-create-org-click
(mf/use-fn (mf/use-fn
(mf/deps profile)
(fn [] (fn []
(if (dnt/is-valid-license? profile) (if (dnt/is-valid-license? profile)
(dnt/go-to-nitrate-cc) (dnt/go-to-nitrate-cc)
(st/emit! (dnt/show-nitrate-popup :nitrate-form)))))] (st/emit! (dnt/show-nitrate-popup :nitrate-form)))))]
(if empty? (if no-orgs?
[:div {:class (stl/css :nitrate-orgs-empty)} [:div {:class (stl/css :nitrate-selected-org)}
[:span {:class (stl/css :nitrate-penpot-icon)} [:span {:class (stl/css :nitrate-penpot-icon)}
[:> raw-svg* {:id penpot-logo-icon}]] [:> raw-svg* {:id penpot-logo-icon}]]
"Penpot" "Penpot"
[:> button* {:variant "ghost" [:> button* {:variant "ghost"
:type "button" :type "button"
:class (stl/css :nitrate-create-org) :class (stl/css :nitrate-create-org)
:on-click on-create-org-click} (tr "dashboard.create-new-org")]] :on-click on-create-org-click} (tr "dashboard.plus-create-new-org")]]
[:div {:class (stl/css :sidebar-team-switch)} [:div {:class (stl/css :sidebar-org-switch)}
[:div {:class (stl/css :switch-content)}
[:button {:class (stl/css :current-team)
:on-click on-show-teams-click
:on-key-down on-show-teams-keydown}
[:div {:class (stl/css :team-name)} [:button {:class (stl/css :current-org)
[:img {:src (cf/resolve-team-photo-url current-org) :on-click on-show-orgs-click
:class (stl/css :team-picture) :on-key-down on-show-orgs-keydown
:alt (:name current-org)}] :aria-expanded show-orgs-menu?
[:span {:class (stl/css :team-text) :title (:name current-org)} (:name current-org)]] :aria-haspopup "menu"}
[:div {:class (stl/css :team-name)}
(if default-org?
[:*
[:span {:class (stl/css :nitrate-penpot-icon)}
[:> raw-svg* {:id penpot-logo-icon}]]
[:span {:class (stl/css :team-text)}
"Penpot"]]
[:*
[:span {:class (stl/css :nitrate-penpot-icon)}
;; TODO org pictures
[:img {:src (cf/resolve-team-photo-url current-org)
:class (stl/css :team-picture)
:alt (:name current-org)}]]
[:span {:class (stl/css :team-text)}
(:name current-org)]])]
arrow-icon]
arrow-icon]] ;; Orgs Dropdown
[:> organizations-selector-dropdown* {:show show-orgs-menu?
;; Teams Dropdown :on-close close-orgs-menu
:id "organizations-list"
[:> teams-selector-dropdown* {:show show-teams-menu? :class (stl/css :dropdown :teams-dropdown)
:on-close close-teams-menu :organization current-org
:id "organizations-list" :profile profile
:class (stl/css :dropdown :teams-dropdown) :organizations orgs}]])))
:team current-org
:profile profile
:teams orgs
:show-default-team false
:allow-create-teams false
:allow-create-org true}]])))
(mf/defc sidebar-team-switch* (mf/defc sidebar-team-switch*
[{:keys [team profile]}] [{:keys [team profile]}]
@ -593,7 +668,9 @@
org-id (when nitrate? (:organization-id team)) org-id (when nitrate? (:organization-id team))
teams (cond->> (mf/deref refs/teams) teams (cond->> (mf/deref refs/teams)
nitrate? nitrate?
(filter #(= (-> % val :organization-id) org-id))) (filter #(= (-> % val :organization-id) org-id))
nitrate?
(into {}))
subscription subscription
(get team :subscription) (get team :subscription)
@ -613,6 +690,9 @@
show-teams-menu? show-teams-menu?
(deref show-teams-menu*) (deref show-teams-menu*)
is-default?
(:is-default team)
on-show-teams-click on-show-teams-click
(mf/use-fn (mf/use-fn
(fn [event] (fn [event]
@ -656,15 +736,17 @@
[:div {:class (stl/css :switch-content)} [:div {:class (stl/css :switch-content)}
[:button {:class (stl/css :current-team) [:button {:class (stl/css :current-team)
:on-click on-show-teams-click :on-click on-show-teams-click
:on-key-down on-show-teams-keydown} :on-key-down on-show-teams-keydown
:aria-expanded show-teams-menu?
:aria-haspopup "menu"}
(cond (cond
(:is-default team) is-default?
[:div {:class (stl/css :team-name)} [:div {:class (stl/css :team-name)}
[:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon] [:span {:class (stl/css :penpot-icon)} deprecated-icon/logo-icon]
[:span {:class (stl/css :team-text)} (tr "dashboard.default-team-name")]] [:span {:class (stl/css :team-text)} (tr "dashboard.default-team-name")]]
(and (contains? cf/flags :subscriptions) (and (contains? cf/flags :subscriptions)
(not (:is-default team)) (not is-default?)
(or (= "unlimited" subscription-type) (= "enterprise" subscription-type))) (or (= "unlimited" subscription-type) (= "enterprise" subscription-type)))
[:div {:class (stl/css :team-name)} [:div {:class (stl/css :team-name)}
[:img {:src (cf/resolve-team-photo-url team) [:img {:src (cf/resolve-team-photo-url team)
@ -675,7 +757,7 @@
[:> menu-team-icon* {:subscription-type subscription-type}]]] [:> menu-team-icon* {:subscription-type subscription-type}]]]
(and (not (:is-default team)) (and (not is-default?)
(or (not= "unlimited" subscription-type) (not= "enterprise" subscription-type))) (or (not= "unlimited" subscription-type) (not= "enterprise" subscription-type)))
[:div {:class (stl/css :team-name)} [:div {:class (stl/css :team-name)}
[:img {:src (cf/resolve-team-photo-url team) [:img {:src (cf/resolve-team-photo-url team)
@ -685,7 +767,7 @@
arrow-icon] arrow-icon]
(when-not (:is-default team) (when-not is-default?
[:button {:class (stl/css :switch-options) [:button {:class (stl/css :switch-options)
:on-click on-show-options-click :on-click on-show-options-click
:aria-label "team-management" :aria-label "team-management"
@ -701,10 +783,7 @@
:class (stl/css :dropdown :teams-dropdown) :class (stl/css :dropdown :teams-dropdown)
:team team :team team
:profile profile :profile profile
:teams teams :teams teams}]
:show-default-team true
:allow-create-teams true
:allow-create-org false}]
[:> team-options-dropdown* {:show show-team-options-menu? [:> team-options-dropdown* {:show show-team-options-menu?
:on-close close-team-options-menu :on-close close-team-options-menu

View File

@ -172,6 +172,14 @@
height: $sz-40; height: $sz-40;
} }
.org-dropdown-item {
@extend .menu-item-base;
display: grid;
grid-template-columns: var(--sp-xxxl) 1fr auto;
gap: var(--sp-s);
height: $sz-40;
}
.action { .action {
--sidebar-action-icon-color: var(--icon-foreground); --sidebar-action-icon-color: var(--icon-foreground);
--sidebar-icon-backgroun-color: var(--color-background-secondary); --sidebar-icon-backgroun-color: var(--color-background-secondary);
@ -492,6 +500,20 @@
stroke: var(--icon-foreground); stroke: var(--icon-foreground);
} }
.add-org-icon {
@extend .button-icon;
width: var(--sp-l);
height: var(--sp-l);
stroke: var(--sidebar-action-icon-color);
}
.arrow-up-right-icon {
@extend .button-icon;
width: var(--sp-m);
height: var(--sp-m);
stroke: var(--sidebar-action-icon-color);
}
.upgrade-plan-section { .upgrade-plan-section {
@include deprecated.buttonStyle; @include deprecated.buttonStyle;
display: flex; display: flex;
@ -530,7 +552,7 @@
// border-block-end: $b-1 solid var(--color-background-quaternary); // border-block-end: $b-1 solid var(--color-background-quaternary);
} }
.nitrate-orgs-empty { .nitrate-selected-org {
@include t.use-typography("body-medium"); @include t.use-typography("body-medium");
color: var(--color-foreground-primary); color: var(--color-foreground-primary);
width: 100%; width: 100%;
@ -560,3 +582,35 @@
height: var(--sp-xxl); height: var(--sp-xxl);
} }
} }
.nitrate-org-icon {
display: flex;
justify-content: center;
align-items: center;
border-radius: 50%;
height: var(--sp-xxl);
width: var(--sp-xxl);
background-color: var(--color-foreground-primary);
svg {
fill: var(--icon-stroke-color);
width: var(--sp-xl);
height: var(--sp-xl);
}
}
.sidebar-org-switch {
position: relative;
width: 100%;
}
.current-org {
@include deprecated.buttonStyle;
display: grid;
align-items: center;
grid-template-columns: 1fr auto;
gap: var(--sp-s);
height: 100%;
width: 100%;
padding: 0 var(--sp-m);
}

View File

@ -54,6 +54,7 @@
(def ^:icon-id arrow-left "arrow-left") (def ^:icon-id arrow-left "arrow-left")
(def ^:icon-id arrow-right "arrow-right") (def ^:icon-id arrow-right "arrow-right")
(def ^:icon-id arrow-up "arrow-up") (def ^:icon-id arrow-up "arrow-up")
(def ^:icon-id arrow-up-right "arrow-up-right")
(def ^:icon-id asc-sort "asc-sort") (def ^:icon-id asc-sort "asc-sort")
(def ^:icon-id at "at") (def ^:icon-id at "at")
(def ^:icon-id board "board") (def ^:icon-id board "board")

View File

@ -359,9 +359,15 @@ msgid "dashboard.copy-suffix"
msgstr "(copy)" msgstr "(copy)"
#: src/app/main/ui/dashboard/sidebar.cljs:347 #: src/app/main/ui/dashboard/sidebar.cljs:347
msgid "dashboard.create-new-org" msgid "dashboard.plus-create-new-org"
msgstr "+ Create org" msgstr "+ Create org"
msgid "dashboard.create-new-org"
msgstr "Create org"
msgid "dashboard.go-to-control-center"
msgstr "Go to Control Center"
#: src/app/main/ui/dashboard/sidebar.cljs:340 #: src/app/main/ui/dashboard/sidebar.cljs:340
msgid "dashboard.create-new-team" msgid "dashboard.create-new-team"
msgstr "Create new team" msgstr "Create new team"

View File

@ -368,9 +368,15 @@ msgid "dashboard.copy-suffix"
msgstr "(copia)" msgstr "(copia)"
#: src/app/main/ui/dashboard/sidebar.cljs:347 #: src/app/main/ui/dashboard/sidebar.cljs:347
msgid "dashboard.create-new-org" msgid "dashboard.plus-create-new-org"
msgstr "+ Crear org" msgstr "+ Crear org"
msgid "dashboard.create-new-org"
msgstr "Crear org"
msgid "dashboard.go-to-control-center"
msgstr "Ir al centro de control"
#: src/app/main/ui/dashboard/sidebar.cljs:340 #: src/app/main/ui/dashboard/sidebar.cljs:340
msgid "dashboard.create-new-team" msgid "dashboard.create-new-team"
msgstr "Crear nuevo equipo" msgstr "Crear nuevo equipo"
@ -2171,7 +2177,7 @@ msgstr "La clave MCP utilizada para conectarse al servidor MCP ha expirado. Como
#: src/app/main/ui/settings/integrations.cljs:368 #: src/app/main/ui/settings/integrations.cljs:368
msgid "integrations.mcp-server.status.expired.1" msgid "integrations.mcp-server.status.expired.1"
msgstr "Por favor, regenera la clave MCP y actualiza la configuración de tu cliente con la nueva clave." msgstr "Por favor, regenera la clave MCP y actualiza la configuración de tu cliente con la nueva clave."
#: src/app/main/ui/settings/integrations.cljs:415 #: src/app/main/ui/settings/integrations.cljs:415
msgid "integrations.mcp-server.mcp-keys.copy" msgid "integrations.mcp-server.mcp-keys.copy"