Add Nitrate's advanced permissions

*  Add Nitrate's advanced permissions

* 📎 Code review
This commit is contained in:
María Valderrama 2026-05-06 10:13:17 +02:00 committed by GitHub
parent 94f8370d98
commit 4ddabaebff
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 285 additions and 83 deletions

View File

@ -348,6 +348,20 @@
[:map [:map
[:cancel-at [:maybe schema:timestamp]]]) [:cancel-at [:maybe schema:timestamp]]])
(defn- get-org-permissions-api
[cfg {:keys [organization-id] :as params}]
(let [baseuri (cf/get :nitrate-backend-uri)]
(request-to-nitrate cfg :get
(str baseuri
"/api/organizations/"
organization-id
"/permissions")
[:map
[:organization-id ::sm/uuid]
[:owner-id ::sm/uuid]
[:create-teams [:enum "any" "onlyMe"]]]
params)))
(defn- redeem-activation-code-api (defn- redeem-activation-code-api
[cfg params] [cfg params]
(let [baseuri (cf/get :nitrate-backend-uri)] (let [baseuri (cf/get :nitrate-backend-uri)]
@ -372,6 +386,7 @@
:add-profile-to-org (partial add-profile-to-org-api cfg) :add-profile-to-org (partial add-profile-to-org-api cfg)
:remove-profile-from-org (partial remove-profile-from-org-api cfg) :remove-profile-from-org (partial remove-profile-from-org-api cfg)
:remove-profile-from-all-orgs (partial remove-profile-from-all-orgs-api cfg) :remove-profile-from-all-orgs (partial remove-profile-from-all-orgs-api cfg)
:get-org-permissions (partial get-org-permissions-api cfg)
:delete-team (partial delete-team-api cfg) :delete-team (partial delete-team-api cfg)
:remove-team-from-org (partial remove-team-from-org-api cfg) :remove-team-from-org (partial remove-team-from-org-api cfg)
:get-subscription (partial get-subscription-api cfg) :get-subscription (partial get-subscription-api cfg)

View File

@ -12,6 +12,7 @@
[app.common.exceptions :as ex] [app.common.exceptions :as ex]
[app.common.schema :as sm] [app.common.schema :as sm]
[app.common.time :as ct] [app.common.time :as ct]
[app.config :as cf]
[app.db :as db] [app.db :as db]
[app.nitrate :as nitrate] [app.nitrate :as nitrate]
[app.rpc :as-alias rpc] [app.rpc :as-alias rpc]
@ -305,6 +306,20 @@
(assert-not-default-team cfg team-id) (assert-not-default-team cfg team-id)
(assert-membership cfg profile-id organization-id) (assert-membership cfg profile-id organization-id)
(when (contains? cf/flags :nitrate)
(let [org-perms (nitrate/call cfg :get-org-permissions
{:organization-id organization-id})]
(if (nil? org-perms)
(ex/raise :type :validation
:code :not-allowed
:hint "Unable to verify organization permissions")
(let [create-perm (:create-teams org-perms)
is-owner? (= profile-id (:owner-id org-perms))]
(when (and (= create-perm "onlyMe") (not is-owner?))
(ex/raise :type :validation
:code :not-allowed
:hint "You are not allowed to add teams in this organization"))))))
(let [team-members (db/query cfg :team-profile-rel {:team-id team-id})] (let [team-members (db/query cfg :team-profile-rel {:team-id team-id})]
;; Add teammates to the org if needed ;; Add teammates to the org if needed
(doseq [{member-id :profile-id} team-members (doseq [{member-id :profile-id} team-members

View File

@ -506,11 +506,27 @@
(sv/defmethod ::create-team (sv/defmethod ::create-team
{::doc/added "1.17" {::doc/added "1.17"
::sm/params schema:create-team} ::sm/params schema:create-team}
[cfg {:keys [::rpc/profile-id] :as params}] [cfg {:keys [::rpc/profile-id organization-id] :as params}]
(quotes/check! cfg {::quotes/id ::quotes/teams-per-profile (quotes/check! cfg {::quotes/id ::quotes/teams-per-profile
::quotes/profile-id profile-id}) ::quotes/profile-id profile-id})
;; When creating inside an org, verify the user has permission to do so.
;; Fail closed: if org permissions cannot be fetched, deny the operation.
(when (and organization-id (contains? cf/flags :nitrate))
(let [org-perms (nitrate/call cfg :get-org-permissions
{:organization-id organization-id})]
(if (nil? org-perms)
(ex/raise :type :validation
:code :not-allowed
:hint "Unable to verify organization permissions")
(let [create-perm (:create-teams org-perms)
is-owner? (= profile-id (:owner-id org-perms))]
(when (and (= create-perm "onlyMe") (not is-owner?))
(ex/raise :type :validation
:code :not-allowed
:hint "You are not allowed to create teams in this organization"))))))
(let [features (-> (cfeat/get-enabled-features cf/flags) (let [features (-> (cfeat/get-enabled-features cf/flags)
(set/difference cfeat/frontend-only-features) (set/difference cfeat/frontend-only-features)
(set/difference cfeat/no-team-inheritable-features)) (set/difference cfeat/no-team-inheritable-features))

View File

@ -15,7 +15,8 @@
[:slug ::sm/text] [:slug ::sm/text]
[:owner-id ::sm/uuid] [:owner-id ::sm/uuid]
[:avatar-bg-url ::sm/uri] [:avatar-bg-url ::sm/uri]
[:logo-id {:optional true} [:maybe ::sm/uuid]]]) [:logo-id {:optional true} [:maybe ::sm/uuid]]
[:create-teams {:optional true} [:maybe [:enum "any" "onlyMe"]]]])
(def schema:team-with-organization (def schema:team-with-organization
@ -31,7 +32,8 @@
[:custom-photo :organization-custom-photo] [:custom-photo :organization-custom-photo]
[:slug :organization-slug] [:slug :organization-slug]
[:avatar-bg-url :organization-avatar-bg-url] [:avatar-bg-url :organization-avatar-bg-url]
[:owner-id :organization-owner-id]]) [:owner-id :organization-owner-id]
[:create-teams :organization-create-teams]])
(defn apply-organization (defn apply-organization
"Updates a team map with organization fields sourced from org. "Updates a team map with organization fields sourced from org.
@ -47,4 +49,4 @@
(assoc acc team-k v) (assoc acc team-k v)
(dissoc acc team-k)))) (dissoc acc team-k))))
team team
organization->team-keys))) organization->team-keys)))

View File

@ -134,3 +134,81 @@
(rx/mapcat (rx/mapcat
(fn [_] (fn [_]
(rx/of (modal/hide)))))))) (rx/of (modal/hide))))))))
(defn show-add-team-to-org-modal
"Fetches fresh team/org data, then shows the add-to-org modal
restricted to orgs where the user has permission, or the no-permission
modal if none qualify."
[{:keys [team-id]}]
(ptk/reify ::show-add-team-to-org-modal
ptk/WatchEvent
(watch [_ state _]
(let [profile-id (dm/get-in state [:profile :id])]
(->> (rp/cmd! :get-teams)
(rx/mapcat
(fn [teams]
(let [all-orgs (map dt/team->organization
(filter #(and (:is-default %) (:organization-id %)) teams))
orgs (filter (fn [org]
(let [perm (:create-teams org)
is-own? (= profile-id (:owner-id org))]
(or (= perm "any") is-own?))) all-orgs)
team (first (filter #(= (:id %) team-id) teams))
on-confirm (fn [organization-id]
(st/emit! (add-team-to-org {:team-id team-id
:organization-id organization-id})))]
(rx/of (dt/teams-fetched teams)
(if (empty? orgs)
(modal/show :no-org-allows-create-team {})
(let [has-filtered? (< (count orgs) (count all-orgs))
extra-props (when has-filtered?
{:info-message-key "dashboard.select-org-modal.permission-info"})]
(modal/show :select-organization-modal
(merge {:organizations orgs
:current-organization-id (:organization-id team)
:on-confirm on-confirm
:title-key "dashboard.select-org-modal.title"
:choose-key "dashboard.select-org-modal.choose"
:placeholder-key "dashboard.select-org-modal.select"
:accept-key "dashboard.select-org-modal.accept"
:cancel-key "labels.cancel"}
extra-props)))))))))))))
(defn show-change-team-org-modal
"Fetches fresh team/org data, then shows the change-org modal
restricted to orgs where the user has permission, or the no-permission
modal if none qualify."
[{:keys [team-id]}]
(ptk/reify ::show-change-team-org-modal
ptk/WatchEvent
(watch [_ state _]
(let [profile-id (dm/get-in state [:profile :id])]
(->> (rp/cmd! :get-teams)
(rx/mapcat
(fn [teams]
(let [all-orgs (map dt/team->organization
(filter #(and (:is-default %) (:organization-id %)) teams))
orgs (filter (fn [org]
(let [perm (:create-teams org)
is-own? (= profile-id (:owner-id org))]
(or (= perm "any") is-own?))) all-orgs)
team (first (filter #(= (:id %) team-id) teams))
on-confirm (fn [organization-id]
(st/emit! (add-team-to-org {:team-id team-id
:organization-id organization-id})))]
(rx/of (dt/teams-fetched teams)
(if (empty? orgs)
(modal/show :no-org-allows-create-team {})
(let [has-filtered? (< (count orgs) (count all-orgs))
extra-props (when has-filtered?
{:info-message-key "dashboard.select-org-modal.permission-info"})]
(modal/show :select-organization-modal
(merge {:organizations orgs
:current-organization-id (:organization-id team)
:on-confirm on-confirm
:title-key "dashboard.change-org-modal.title"
:choose-key "dashboard.change-org-modal.choose"
:placeholder-key "dashboard.change-org-modal.select"
:accept-key "dashboard.change-org-modal.accept"
:cancel-key "labels.cancel"}
extra-props)))))))))))))

View File

@ -16,6 +16,7 @@
[app.config :as cf] [app.config :as cf]
[app.main.data.event :as ev] [app.main.data.event :as ev]
[app.main.data.media :as di] [app.main.data.media :as di]
[app.main.data.modal :as modal]
[app.main.data.profile :as dp] [app.main.data.profile :as dp]
[app.main.features :as features] [app.main.features :as features]
[app.main.repo :as rp] [app.main.repo :as rp]
@ -50,7 +51,8 @@
:organization-name :organization-name
:organization-slug :organization-slug
:organization-owner-id :organization-owner-id
:organization-avatar-bg-url))] :organization-avatar-bg-url
:organization-create-teams))]
(update state :teams assoc id team-updated))) (update state :teams assoc id team-updated)))
state state
teams))))) teams)))))
@ -63,6 +65,30 @@
(->> (rp/cmd! :get-teams) (->> (rp/cmd! :get-teams)
(rx/map teams-fetched))))) (rx/map teams-fetched)))))
(defn check-and-create-team
"Fetches fresh team data from the server to ensure up-to-date org
permissions, then shows the team-form modal or a no-permission modal."
[team-id]
(ptk/reify ::check-and-create-team
ptk/WatchEvent
(watch [_ state _]
(let [profile-id (dm/get-in state [:profile :id])]
(->> (rp/cmd! :get-teams)
(rx/mapcat
(fn [teams]
(let [team (d/seek #(= (:id %) team-id) teams)
in-org? (and (contains? cf/flags :nitrate) (:organization-id team))
create-perm (:organization-create-teams team)
is-owner? (= profile-id (:organization-owner-id team))
can-create? (or (not in-org?) (= create-perm "any") is-owner?)]
(rx/of (teams-fetched teams)
(if can-create?
(modal/show :team-form (if in-org?
{:organization-id (:organization-id team)
:organization-name (:organization-name team)}
{}))
(modal/show :no-permission-create-team {:organization-name (:organization-name team)})))))))))))
;; --- EVENT: fetch-members ;; --- EVENT: fetch-members
(defn- members-fetched (defn- members-fetched
@ -598,5 +624,6 @@
:avatar-bg-url (:organization-avatar-bg-url team) :avatar-bg-url (:organization-avatar-bg-url team)
:custom-photo (:organization-custom-photo team) :custom-photo (:organization-custom-photo team)
:name (:organization-name team) :name (:organization-name team)
:default-team-id (:id team)}) :default-team-id (:id team)
:create-teams (:organization-create-teams team)})

View File

@ -384,10 +384,9 @@
(mf/use-fn (mf/use-fn
(mf/deps team) (mf/deps team)
(fn [] (fn []
(let [params (if (and (contains? cf/flags :nitrate) (:organization-id team)) (if (contains? cf/flags :nitrate)
{:organization-id (:organization-id team)} (st/emit! (dtm/check-and-create-team (:id team)))
{})] (st/emit! (modal/show :team-form {})))))
(st/emit! (modal/show :team-form params)))))
on-team-click on-team-click
(mf/use-fn (mf/use-fn

View File

@ -810,7 +810,7 @@
(mf/defc select-organization-modal (mf/defc select-organization-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :select-organization-modal} ::mf/register-as :select-organization-modal}
[{:keys [organizations current-organization-id on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key]}] [{:keys [organizations current-organization-id on-confirm title-key text-key choose-key placeholder-key accept-key cancel-key info-message-key]}]
(let [valid-organizations (mf/with-memo [organizations] (let [valid-organizations (mf/with-memo [organizations]
(remove #(= (:id %) current-organization-id) organizations)) (remove #(= (:id %) current-organization-id) organizations))
options (mf/with-memo [valid-organizations] options (mf/with-memo [valid-organizations]
@ -848,6 +848,9 @@
[:div {:class (stl/css :modal-content :modal-select-org-text)} (tr text-key)]) [:div {:class (stl/css :modal-content :modal-select-org-text)} (tr text-key)])
[:div [:div
(when info-message-key
[:div {:class (stl/css :modal-select-org-info)}
(tr info-message-key)])
[:div {:class (stl/css :modal-select-org-content)} [:div {:class (stl/css :modal-select-org-content)}
(tr choose-key)] (tr choose-key)]
[:> combobox* {:id "selected-id" [:> combobox* {:id "selected-id"
@ -1366,18 +1369,29 @@
can-edit (or (:is-owner permissions) can-edit (or (:is-owner permissions)
(:is-admin permissions)) (:is-admin permissions))
organizations (mf/deref refs/teams) profile (mf/deref refs/profile)
organizations (mf/with-memo [organizations] profile-id (:id profile)
(->> (vals organizations)
(filter :is-default) all-organizations (mf/deref refs/teams)
(filter :organization-id) all-organizations (mf/with-memo [all-organizations]
(map dtm/team->organization))) (->> (vals all-organizations)
(filter :is-default)
(filter :organization-id)
(map dtm/team->organization)))
;; Filter to orgs where user is allowed to create/add teams
organizations (mf/with-memo [all-organizations profile-id]
(->> all-organizations
(filter (fn [org]
(let [perm (:create-teams org)
is-owner? (= profile-id (:owner-id org))]
(or (= perm "any") is-owner?))))))
can-change-organization? (mf/with-memo [organizations] can-change-organization? (mf/with-memo [organizations]
(> (count organizations) 1)) (> (count organizations) 1))
can-add-to-organization? (mf/with-memo [organizations] can-add-to-organization? (mf/with-memo [organizations all-organizations]
(and (pos? (count organizations)) (and (pos? (count all-organizations))
(not (:is-default team)))) (not (:is-default team))))
show-org-options-menu* show-org-options-menu*
@ -1424,41 +1438,17 @@
:accept-style :danger}] :accept-style :danger}]
(st/emit! (modal/show params))))) (st/emit! (modal/show params)))))
on-add-team-to-org-confirm
(mf/use-fn
(mf/deps team)
(fn [organization-id]
(let [organization (d/seek #(= organization-id (:id %)) organizations)]
(when organization
(st/emit! (dnt/add-team-to-org {:team-id (:id team)
:organization-id organization-id}))))))
on-add-team-to-org on-add-team-to-org
(mf/use-fn (mf/use-fn
(mf/deps organizations on-add-team-to-org-confirm) (mf/deps team)
(fn [] (fn []
(st/emit! (modal/show :select-organization-modal {:organizations organizations (st/emit! (dnt/show-add-team-to-org-modal {:team-id (:id team)}))))
:current-organization-id (:organization-id team)
:on-confirm on-add-team-to-org-confirm
:title-key "dashboard.select-org-modal.title"
:choose-key "dashboard.select-org-modal.choose"
:placeholder-key "dashboard.select-org-modal.select"
:accept-key "dashboard.select-org-modal.accept"
:cancel-key "labels.cancel"}))))
on-change-team-org on-change-team-org
(mf/use-fn (mf/use-fn
(mf/deps organizations on-add-team-to-org-confirm) (mf/deps team)
(fn [] (fn []
(st/emit! (modal/show :select-organization-modal {:organizations organizations (st/emit! (dnt/show-change-team-org-modal {:team-id (:id team)}))))]
:current-organization-id (:organization-id team)
:on-confirm on-add-team-to-org-confirm
:title-key "dashboard.change-org-modal.title"
:text-key "dashboard.change-org-modal.text"
:choose-key "dashboard.change-org-modal.choose"
:placeholder-key "dashboard.change-org-modal.select"
:accept-key "dashboard.change-org-modal.accept"
:cancel-key "labels.cancel"}))))]
(mf/with-effect [team] (mf/with-effect [team]
(dom/set-html-title (tr "title.team-settings" (dom/set-html-title (tr "title.team-settings"

View File

@ -887,6 +887,13 @@
margin-block-end: var(--sp-s); margin-block-end: var(--sp-s);
} }
.modal-select-org-info {
@include t.use-typography("body-medium");
color: var(--color-foreground-secondary);
margin-block-end: var(--sp-xxxl);
}
.modal-select-org-title { .modal-select-org-title {
@include t.use-typography("title-medium"); @include t.use-typography("title-medium");

View File

@ -42,16 +42,19 @@
(modal/hide)))) (modal/hide))))
(defn- on-error (defn- on-error
[form _response] [form organization-name response]
(let [id (get-in @form [:clean-data :id])] (let [id (get-in @form [:clean-data :id])
(if id code (-> response ex-data :code)]
(rx/of (ntf/error "Error on updating team.")) (if (= code :not-allowed)
(rx/of (ntf/error "Error on creating team."))))) (rx/of (modal/show :no-permission-create-team {:organization-name organization-name}))
(if id
(rx/of (ntf/error "Error on updating team."))
(rx/of (ntf/error "Error on creating team."))))))
(defn- on-create-submit (defn- on-create-submit
[form] [form organization-name]
(let [mdata {:on-success (partial on-create-success form) (let [mdata {:on-success (partial on-create-success form)
:on-error (partial on-error form)} :on-error (partial on-error form organization-name)}
data (:clean-data @form) data (:clean-data @form)
params (cond-> {:name (:name data)} params (cond-> {:name (:name data)}
(:organization-id data) (assoc :organization-id (:organization-id data)))] (:organization-id data) (assoc :organization-id (:organization-id data)))]
@ -61,23 +64,23 @@
(defn- on-update-submit (defn- on-update-submit
[form] [form]
(let [mdata {:on-success (partial on-update-success form) (let [mdata {:on-success (partial on-update-success form)
:on-error (partial on-error form)} :on-error (partial on-error form nil)}
data (:clean-data @form) data (:clean-data @form)
team (select-keys data [:id :name])] ;; Only send name and id for updates team (select-keys data [:id :name])]
(st/emit! (dtm/update-team (with-meta team mdata)) (st/emit! (dtm/update-team (with-meta team mdata))
(modal/hide)))) (modal/hide))))
(defn- on-submit (defn- on-submit
[form _] [organization-name form _]
(let [data (:clean-data @form)] (let [data (:clean-data @form)]
(if (:id data) (if (:id data)
(on-update-submit form) (on-update-submit form)
(on-create-submit form)))) (on-create-submit form organization-name))))
(mf/defc team-form-modal (mf/defc team-form-modal
{::mf/register modal/components {::mf/register modal/components
::mf/register-as :team-form} ::mf/register-as :team-form}
[{:keys [team organization-id] :as props}] [{:keys [team organization-id organization-name] :as props}]
(let [initial (mf/use-memo (let [initial (mf/use-memo
(mf/deps team organization-id) (mf/deps team organization-id)
(fn [] (fn []
@ -89,18 +92,22 @@
organization-id (assoc :organization-id organization-id))))) organization-id (assoc :organization-id organization-id)))))
form (fm/use-form :schema schema:team-form form (fm/use-form :schema schema:team-form
:initial initial) :initial initial)
on-submit* (mf/use-fn
(mf/deps organization-name)
(partial on-submit organization-name))
handle-keydown handle-keydown
(mf/use-fn (mf/use-fn
(mf/deps organization-name)
(fn [e] (fn [e]
(when (kbd/enter? e) (when (kbd/enter? e)
(dom/prevent-default e) (dom/prevent-default e)
(dom/stop-propagation e) (dom/stop-propagation e)
(on-submit form e))))] (on-submit organization-name form e))))]
[:div {:class (stl/css :modal-overlay)} [:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)} [:div {:class (stl/css :modal-container)}
[:& fm/form {:form form [:& fm/form {:form form
:on-submit on-submit :on-submit on-submit*
:class (stl/css :team-form)} :class (stl/css :team-form)}
[:div {:class (stl/css :modal-header)} [:div {:class (stl/css :modal-header)}
@ -132,3 +139,31 @@
:class (stl/css :accept-btn)}]]]]]])) :class (stl/css :accept-btn)}]]]]]]))
(mf/defc no-permission-create-team-modal*
{::mf/register modal/components
::mf/register-as :no-permission-create-team}
[{:keys [organization-name]}]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)}
(tr "labels.create-team")]
[:button {:class (stl/css :modal-close-btn)
:on-click modal/hide!} deprecated-icon/close]]
[:div {:class (stl/css :modal-content)}
[:div (tr "dashboard.no-permission-create-team.message" organization-name)]]]])
(mf/defc no-org-allows-create-team-modal*
{::mf/register modal/components
::mf/register-as :no-org-allows-create-team}
[_props]
[:div {:class (stl/css :modal-overlay)}
[:div {:class (stl/css :modal-container)}
[:div {:class (stl/css :modal-header)}
[:h2 {:class (stl/css :modal-title)}
(tr "dashboard.select-org-modal.title")]
[:button {:class (stl/css :modal-close-btn)
:on-click modal/hide!} deprecated-icon/close]]
[:div {:class (stl/css :modal-content)}
[:div (tr "dashboard.no-org-allows-create-team.message")]]]])

View File

@ -5,6 +5,8 @@
// Copyright (c) KALEIDOS INC // Copyright (c) KALEIDOS INC
@use "refactor/common-refactor.scss" as deprecated; @use "refactor/common-refactor.scss" as deprecated;
@use "ds/_utils.scss" as *;
@use "ds/typography.scss" as t;
.modal-overlay { .modal-overlay {
@extend %modal-overlay-base; @extend %modal-overlay-base;
@ -15,11 +17,11 @@
} }
.modal-header { .modal-header {
margin-bottom: deprecated.$s-24; margin-block-end: var(--sp-xxl);
} }
.modal-title { .modal-title {
@include deprecated.uppercase-title-typography; @include t.use-typography("headline-large");
color: var(--modal-title-foreground-color); color: var(--modal-title-foreground-color);
} }
@ -29,33 +31,31 @@
} }
.modal-content { .modal-content {
margin-bottom: deprecated.$s-24; margin-block-end: var(--sp-xxl);
color: var(--color-foreground-secondary);
@include t.use-typography("body-large");
} }
.team-form { .team-form {
min-width: deprecated.$s-400; min-inline-size: px2rem(400);
} }
.group-name-input { .group-name-input {
@extend %input-element-label; @extend %input-element-label;
@include deprecated.body-small-typography;
margin-bottom: deprecated.$s-8; margin-block-end: var(--sp-s);
}
label { .group-name-input label {
@include deprecated.flex-column; display: flex;
@include deprecated.body-small-typography; flex-direction: column;
gap: var(--sp-xs);
align-items: flex-start; align-items: flex-start;
width: 100%; width: 100%;
border: none; border: none;
background-color: transparent; background-color: transparent;
height: 100%; height: 100%;
input {
@include deprecated.body-small-typography;
}
}
} }
.action-buttons { .action-buttons {

View File

@ -9395,4 +9395,13 @@ msgid "subscription.current-plan.nitrate-trial"
msgstr "Nitrate Trial" msgstr "Nitrate Trial"
msgid "subscription.current-plan.enterprise" msgid "subscription.current-plan.enterprise"
msgstr "Enterprise" msgstr "Enterprise"
msgid "dashboard.no-permission-create-team.message"
msgstr "You are not allowed to create teams within %s organization. If you need more information, contact the owner."
msgid "dashboard.no-org-allows-create-team.message"
msgstr "You don't have permission to add teams to any of your organizations."
msgid "dashboard.select-org-modal.permission-info"
msgstr "Here you find all your organizations where you are allowed to create or add teams."

View File

@ -9103,4 +9103,13 @@ msgid "subscription.current-plan.nitrate-trial"
msgstr "Nitrate (Prueba)" msgstr "Nitrate (Prueba)"
msgid "subscription.current-plan.enterprise" msgid "subscription.current-plan.enterprise"
msgstr "Enterprise" msgstr "Enterprise"
msgid "dashboard.no-permission-create-team.message"
msgstr "No tienes permiso para crear equipos en la organización %s. Si necesitas más información, contacta con el propietario."
msgid "dashboard.no-org-allows-create-team.message"
msgstr "No tienes permiso para añadir equipos a ninguna de tus organizaciones."
msgid "dashboard.select-org-modal.permission-info"
msgstr "Aquí encontrarás todas las organizaciones en las que tienes permiso para crear o añadir equipos."