mirror of
https://github.com/penpot/penpot.git
synced 2026-05-30 12:18:13 +00:00
✨ Check nitrate permission only org members for move teams
This commit is contained in:
parent
5c93ad0ab3
commit
a637dda554
@ -516,3 +516,36 @@
|
|||||||
team-member-ids (into #{} (map :profile-id team-members))]
|
team-member-ids (into #{} (map :profile-id team-members))]
|
||||||
(every? #(contains? team-member-ids %) org-member-ids)))
|
(every? #(contains? team-member-ids %) org-member-ids)))
|
||||||
false))
|
false))
|
||||||
|
|
||||||
|
(def ^:private schema:all-team-members-in-orgs-params
|
||||||
|
[:map {:title "CheckTeamMembersInOrgsParams"}
|
||||||
|
[:team-id ::sm/uuid]
|
||||||
|
[:organization-ids [:vector ::sm/uuid]]])
|
||||||
|
|
||||||
|
(sv/defmethod ::all-team-members-in-orgs
|
||||||
|
{::rpc/auth true
|
||||||
|
::doc/added "2.17"
|
||||||
|
::sm/params schema:all-team-members-in-orgs-params
|
||||||
|
::sm/result [:map-of ::sm/uuid ::sm/boolean]}
|
||||||
|
[cfg {:keys [::rpc/profile-id team-id organization-ids]}]
|
||||||
|
(if (contains? cf/flags :nitrate)
|
||||||
|
(let [perms (teams/get-permissions cfg profile-id team-id)]
|
||||||
|
(when-not (or (:is-admin perms) (:is-owner perms))
|
||||||
|
(ex/raise :type :validation
|
||||||
|
:code :insufficient-permissions))
|
||||||
|
|
||||||
|
(let [team-members (db/query cfg :team-profile-rel {:team-id team-id})
|
||||||
|
team-member-ids (into #{} (map :profile-id team-members))]
|
||||||
|
;; Validate requester membership in all orgs before fetching members.
|
||||||
|
(run! #(assert-membership cfg profile-id %) organization-ids)
|
||||||
|
|
||||||
|
(into {}
|
||||||
|
(map (fn [organization-id]
|
||||||
|
(let [org-members (nitrate/call cfg :get-org-members {:organization-id organization-id})
|
||||||
|
org-member-ids (into #{} org-members)]
|
||||||
|
[organization-id
|
||||||
|
(every? #(contains? org-member-ids %) team-member-ids)])))
|
||||||
|
organization-ids)))
|
||||||
|
{}))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
(ns backend-tests.rpc-nitrate-test
|
(ns backend-tests.rpc-nitrate-test
|
||||||
(:require
|
(:require
|
||||||
[app.common.uuid :as uuid]
|
[app.common.uuid :as uuid]
|
||||||
|
[app.config :as cf]
|
||||||
[app.db :as-alias db]
|
[app.db :as-alias db]
|
||||||
[app.nitrate :as nitrate]
|
[app.nitrate :as nitrate]
|
||||||
[app.rpc :as-alias rpc]
|
[app.rpc :as-alias rpc]
|
||||||
@ -715,6 +716,71 @@
|
|||||||
(t/is (= :validation (th/ex-type (:error out))))
|
(t/is (= :validation (th/ex-type (:error out))))
|
||||||
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
(t/is (= :not-valid-teams (th/ex-code (:error out))))))))
|
||||||
|
|
||||||
|
(t/deftest all-team-members-in-orgs-returns-org-id->boolean-map
|
||||||
|
(let [profile-user (th/create-profile* 201 {:is-active true})
|
||||||
|
profile-other (th/create-profile* 202 {:is-active true})
|
||||||
|
team (th/create-team* 201 {:profile-id (:id profile-user)})
|
||||||
|
_ (th/create-team-role* {:team-id (:id team)
|
||||||
|
:profile-id (:id profile-other)
|
||||||
|
:role :editor})
|
||||||
|
team-member-ids (->> (th/db-query :team-profile-rel {:team-id (:id team)})
|
||||||
|
(map :profile-id)
|
||||||
|
(into #{}))
|
||||||
|
org-id-1 (uuid/random)
|
||||||
|
org-id-2 (uuid/random)
|
||||||
|
calls (atom [])]
|
||||||
|
(with-redefs [cf/flags (conj cf/flags :nitrate)
|
||||||
|
nitrate/call (fn [_cfg method params]
|
||||||
|
(swap! calls conj [method params])
|
||||||
|
(case method
|
||||||
|
:get-org-membership {:is-member true
|
||||||
|
:organization-id (:organization-id params)}
|
||||||
|
:get-org-members (get {org-id-1 (vec team-member-ids)
|
||||||
|
org-id-2 [(:id profile-user)]}
|
||||||
|
(:organization-id params)
|
||||||
|
[])
|
||||||
|
nil))]
|
||||||
|
(let [out (th/command! {::th/type :all-team-members-in-orgs
|
||||||
|
::rpc/profile-id (:id profile-user)
|
||||||
|
:team-id (:id team)
|
||||||
|
:organization-ids [org-id-1 org-id-2]})
|
||||||
|
methods (map first @calls)
|
||||||
|
membership-calls (count (filter #(= :get-org-membership %) methods))
|
||||||
|
get-members-calls (count (filter #(= :get-org-members %) methods))]
|
||||||
|
(t/is (th/success? out))
|
||||||
|
(t/is (= {org-id-1 true
|
||||||
|
org-id-2 false}
|
||||||
|
(:result out)))
|
||||||
|
(t/is (= 2 membership-calls))
|
||||||
|
(t/is (= 2 get-members-calls))))))
|
||||||
|
|
||||||
|
(t/deftest all-team-members-in-orgs-fails-before-fetching-org-members
|
||||||
|
(let [profile-user (th/create-profile* 203 {:is-active true})
|
||||||
|
team (th/create-team* 203 {:profile-id (:id profile-user)})
|
||||||
|
org-id-1 (uuid/random)
|
||||||
|
org-id-2 (uuid/random)
|
||||||
|
calls (atom [])]
|
||||||
|
(with-redefs [cf/flags (conj cf/flags :nitrate)
|
||||||
|
nitrate/call (fn [_cfg method params]
|
||||||
|
(swap! calls conj [method params])
|
||||||
|
(case method
|
||||||
|
:get-org-membership (if (= (:organization-id params) org-id-2)
|
||||||
|
{:is-member false
|
||||||
|
:organization-id (:organization-id params)}
|
||||||
|
{:is-member true
|
||||||
|
:organization-id (:organization-id params)})
|
||||||
|
:get-org-members []
|
||||||
|
nil))]
|
||||||
|
(let [out (th/command! {::th/type :all-team-members-in-orgs
|
||||||
|
::rpc/profile-id (:id profile-user)
|
||||||
|
:team-id (:id team)
|
||||||
|
:organization-ids [org-id-1 org-id-2]})
|
||||||
|
methods (map first @calls)]
|
||||||
|
(t/is (not (th/success? out)))
|
||||||
|
(t/is (= :validation (th/ex-type (:error out))))
|
||||||
|
(t/is (= :user-doesnt-belong-organization (th/ex-code (:error out))))
|
||||||
|
(t/is (= 0 (count (filter #(= :get-org-members %) methods))))))))
|
||||||
|
|
||||||
(t/deftest leave-org-error-reassign-on-non-owned-team
|
(t/deftest leave-org-error-reassign-on-non-owned-team
|
||||||
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
(let [profile-owner (th/create-profile* 1 {:is-active true})
|
||||||
profile-user (th/create-profile* 2 {:is-active true})
|
profile-user (th/create-profile* 2 {:is-active true})
|
||||||
|
|||||||
@ -247,25 +247,69 @@
|
|||||||
is-own? (= profile-id (:owner-id org))]
|
is-own? (= profile-id (:owner-id org))]
|
||||||
(or (= perm "any") is-own?))) all-orgs)
|
(or (= perm "any") is-own?))) all-orgs)
|
||||||
team (first (filter #(= (:id %) team-id) teams))
|
team (first (filter #(= (:id %) team-id) teams))
|
||||||
|
add-anybody-to-team-orgs
|
||||||
|
(filterv #(nitrate-perms/allowed? :add-anybody-to-team
|
||||||
|
{:org-perms %})
|
||||||
|
orgs)
|
||||||
|
orgs-to-check
|
||||||
|
(filterv #(not (nitrate-perms/allowed? :add-anybody-to-team
|
||||||
|
{:org-perms %}))
|
||||||
|
orgs)
|
||||||
|
org-ids-to-check (mapv :id orgs-to-check)
|
||||||
on-confirm (fn [organization-id]
|
on-confirm (fn [organization-id]
|
||||||
(st/emit! (add-team-to-org {:team-id team-id
|
(st/emit! (add-team-to-org {:team-id team-id
|
||||||
:organization-id organization-id})))]
|
:organization-id organization-id})))
|
||||||
(rx/of (dt/teams-fetched teams)
|
show-select-modal
|
||||||
(if (empty? orgs)
|
(fn [orgs-allowed]
|
||||||
(modal/show :no-permission-modal {:type :no-orgs-create})
|
(let [has-filtered? (< (count orgs) (count all-orgs))
|
||||||
(let [has-filtered? (< (count orgs) (count all-orgs))
|
extra-props (when has-filtered?
|
||||||
extra-props (when has-filtered?
|
{:info-message-key "dashboard.select-org-modal.permission-info"})]
|
||||||
{:info-message-key "dashboard.select-org-modal.permission-info"})]
|
(modal/show :select-organization-modal
|
||||||
(modal/show :select-organization-modal
|
(merge {:organizations orgs
|
||||||
(merge {:organizations orgs
|
:orgs-allowed orgs-allowed
|
||||||
:current-organization-id (dm/get-in team [:organization :id])
|
:current-organization-id (dm/get-in team [:organization :id])
|
||||||
:on-confirm on-confirm
|
:on-confirm on-confirm
|
||||||
:title-key "dashboard.select-org-modal.title"
|
:title-key "dashboard.select-org-modal.title"
|
||||||
:choose-key "dashboard.select-org-modal.choose"
|
:choose-key "dashboard.select-org-modal.choose"
|
||||||
:placeholder-key "dashboard.select-org-modal.select"
|
:placeholder-key "dashboard.select-org-modal.select"
|
||||||
:accept-key "dashboard.select-org-modal.accept"
|
:accept-key "dashboard.select-org-modal.accept"
|
||||||
:cancel-key "labels.cancel"}
|
:cancel-key "labels.cancel"}
|
||||||
extra-props)))))))))))))
|
extra-props))))]
|
||||||
|
(cond
|
||||||
|
(empty? orgs)
|
||||||
|
(rx/of (dt/teams-fetched teams)
|
||||||
|
(modal/show :no-permission-modal {:type :no-orgs-create}))
|
||||||
|
|
||||||
|
(empty? org-ids-to-check)
|
||||||
|
(let [orgs-allowed (into {}
|
||||||
|
(map (fn [org] [(:id org) true]))
|
||||||
|
orgs)]
|
||||||
|
(rx/of (dt/teams-fetched teams)
|
||||||
|
(show-select-modal orgs-allowed)))
|
||||||
|
|
||||||
|
:else
|
||||||
|
(->> (rp/cmd! :all-team-members-in-orgs
|
||||||
|
{:team-id team-id
|
||||||
|
:organization-ids org-ids-to-check})
|
||||||
|
(rx/mapcat
|
||||||
|
(fn [checked-orgs]
|
||||||
|
(let [orgs-allowed
|
||||||
|
(merge (into {}
|
||||||
|
(map (fn [org] [(:id org) true]))
|
||||||
|
add-anybody-to-team-orgs)
|
||||||
|
checked-orgs)
|
||||||
|
valid-orgs
|
||||||
|
(filterv #(true? (get orgs-allowed (:id %))) orgs)]
|
||||||
|
(rx/of
|
||||||
|
(dt/teams-fetched teams)
|
||||||
|
(if (empty? valid-orgs)
|
||||||
|
(modal/show
|
||||||
|
{:type :alert
|
||||||
|
:message (tr "dashboard.team-organization.add.no-valid-orgs")
|
||||||
|
:accept-label (tr "labels.accept")
|
||||||
|
:accept-style :primary
|
||||||
|
:title (tr "dashboard.select-org-modal.title")})
|
||||||
|
(show-select-modal orgs-allowed))))))))))))))))
|
||||||
|
|
||||||
(defn show-change-team-org-modal
|
(defn show-change-team-org-modal
|
||||||
"Fetches fresh team/org data, then shows the change-org modal
|
"Fetches fresh team/org data, then shows the change-org modal
|
||||||
|
|||||||
@ -883,16 +883,24 @@
|
|||||||
(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 info-message-key]}]
|
[{:keys [organizations orgs-allowed 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 orgs-allowed]
|
||||||
(mapv (fn [organization]
|
(mapv (fn [organization]
|
||||||
{:id (str (:id organization))
|
(let [org-id (:id organization)
|
||||||
:label (:name organization)
|
;; orgs-allowed is a map of org-id and a boolean indicating if it is allowed
|
||||||
:avatar {:render-fn render-org-combobox-avatar*
|
enabled? (or (nil? orgs-allowed)
|
||||||
:organization organization
|
(true? (get orgs-allowed org-id)))]
|
||||||
:size "xl"}})
|
(cond-> {:id (str org-id)
|
||||||
|
:label (:name organization)
|
||||||
|
:disabled (not enabled?)
|
||||||
|
:dimmed (not enabled?)
|
||||||
|
:avatar {:render-fn render-org-combobox-avatar*
|
||||||
|
:organization organization
|
||||||
|
:size "xl"}}
|
||||||
|
(not enabled?)
|
||||||
|
(assoc :title (tr "dashboard.team-organization.disabled-org-tooltip")))))
|
||||||
valid-organizations))
|
valid-organizations))
|
||||||
|
|
||||||
form (fm/use-form :schema schema:organization-form :initial {})
|
form (fm/use-form :schema schema:organization-form :initial {})
|
||||||
|
|||||||
@ -93,16 +93,18 @@
|
|||||||
|
|
||||||
on-option-click
|
on-option-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
(mf/deps on-change)
|
(mf/deps on-change options)
|
||||||
(fn [event]
|
(fn [event]
|
||||||
(dom/stop-propagation event)
|
(dom/stop-propagation event)
|
||||||
(let [node (dom/get-current-target event)
|
(let [node (dom/get-current-target event)
|
||||||
id (dom/get-data node "id")]
|
id (dom/get-data node "id")
|
||||||
(reset! selected-id* id)
|
option (d/seek #(= id (get % :id)) options)]
|
||||||
(reset! is-open* false)
|
(when-not (true? (:disabled option))
|
||||||
(reset! focused-id* nil)
|
(reset! selected-id* id)
|
||||||
(when (fn? on-change)
|
(reset! is-open* false)
|
||||||
(on-change id)))))
|
(reset! focused-id* nil)
|
||||||
|
(when (fn? on-change)
|
||||||
|
(on-change id))))))
|
||||||
|
|
||||||
on-click
|
on-click
|
||||||
(mf/use-fn
|
(mf/use-fn
|
||||||
@ -192,14 +194,15 @@
|
|||||||
(handle-focus-change options focused-id* new-index nodes))
|
(handle-focus-change options focused-id* new-index nodes))
|
||||||
|
|
||||||
(kbd/enter? event)
|
(kbd/enter? event)
|
||||||
(do
|
(let [focused-option (d/seek #(= focused-id (get % :id)) options)]
|
||||||
(reset! selected-id* focused-id)
|
(when-not (true? (:disabled focused-option))
|
||||||
(reset! is-open* false)
|
(reset! selected-id* focused-id)
|
||||||
(reset! focused-id* nil)
|
(reset! is-open* false)
|
||||||
(dom/blur! (mf/ref-val input-ref))
|
(reset! focused-id* nil)
|
||||||
(when (and (fn? on-change)
|
(dom/blur! (mf/ref-val input-ref))
|
||||||
(some? focused-id))
|
(when (and (fn? on-change)
|
||||||
(on-change focused-id)))
|
(some? focused-id))
|
||||||
|
(on-change focused-id))))
|
||||||
|
|
||||||
(kbd/esc? event)
|
(kbd/esc? event)
|
||||||
(do (reset! is-open* false)
|
(do (reset! is-open* false)
|
||||||
|
|||||||
@ -20,8 +20,10 @@
|
|||||||
[:icon {:optional true} [:maybe :string]]
|
[:icon {:optional true} [:maybe :string]]
|
||||||
[:selected {:optional true} :boolean]
|
[:selected {:optional true} :boolean]
|
||||||
[:focused {:optional true} :boolean]
|
[:focused {:optional true} :boolean]
|
||||||
|
[:disabled {:optional true} :boolean]
|
||||||
[:dimmed {:optional true} :boolean]
|
[:dimmed {:optional true} :boolean]
|
||||||
[:label {:optional true} :string]
|
[:label {:optional true} :string]
|
||||||
|
[:title {:optional true} [:maybe :string]]
|
||||||
[:avatar {:optional true}
|
[:avatar {:optional true}
|
||||||
[:maybe
|
[:maybe
|
||||||
[:map
|
[:map
|
||||||
@ -39,23 +41,26 @@
|
|||||||
|
|
||||||
(mf/defc option*
|
(mf/defc option*
|
||||||
{::mf/schema schema:option}
|
{::mf/schema schema:option}
|
||||||
[{:keys [id ref label icon avatar aria-label on-click selected focused dimmed] :rest props}]
|
[{:keys [id ref label icon avatar aria-label on-click selected focused disabled dimmed title] :rest props}]
|
||||||
(let [render-avatar-fn (when avatar
|
(let [render-avatar-fn (when avatar
|
||||||
(get avatar :render-fn))
|
(get avatar :render-fn))
|
||||||
|
|
||||||
class (stl/css-case :option true
|
class (stl/css-case :option true
|
||||||
:option-with-icon (some? icon)
|
:option-with-icon (some? icon)
|
||||||
:option-with-avatar (fn? render-avatar-fn)
|
:option-with-avatar (fn? render-avatar-fn)
|
||||||
|
:option-disabled disabled
|
||||||
:option-selected selected
|
:option-selected selected
|
||||||
:option-current focused)]
|
:option-current focused)]
|
||||||
|
|
||||||
[:li {:value id
|
[:li {:value id
|
||||||
:class class
|
:class class
|
||||||
:aria-selected selected
|
:aria-selected selected
|
||||||
|
:aria-disabled disabled
|
||||||
:ref ref
|
:ref ref
|
||||||
:role "option"
|
:role "option"
|
||||||
:id id
|
:id id
|
||||||
:on-click on-click
|
:title title
|
||||||
|
:on-click (when-not disabled on-click)
|
||||||
:data-id id
|
:data-id id
|
||||||
:data-testid "dropdown-option"}
|
:data-testid "dropdown-option"}
|
||||||
|
|
||||||
|
|||||||
@ -71,6 +71,10 @@
|
|||||||
--options-icon-fg-color: var(--color-accent-primary);
|
--options-icon-fg-color: var(--color-accent-primary);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.option-disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
}
|
||||||
|
|
||||||
.option-check {
|
.option-check {
|
||||||
color: var(--token-options-icon-fg-color);
|
color: var(--token-options-icon-fg-color);
|
||||||
min-width: var(--sp-l);
|
min-width: var(--sp-l);
|
||||||
|
|||||||
@ -37,11 +37,14 @@
|
|||||||
[:value {:optional true} :keyword]
|
[:value {:optional true} :keyword]
|
||||||
[:icon {:optional true} schema:icon-list]
|
[:icon {:optional true} schema:icon-list]
|
||||||
[:label {:optional true} :string]
|
[:label {:optional true} :string]
|
||||||
|
[:title {:optional true} :string]
|
||||||
[:avatar {:optional true}
|
[:avatar {:optional true}
|
||||||
[:map
|
[:map
|
||||||
[:size {:optional true} :string]
|
[:size {:optional true} :string]
|
||||||
[:organization {:optional true} :any]
|
[:organization {:optional true} :any]
|
||||||
[:render-fn {:optional true} fn?]]]
|
[:render-fn {:optional true} fn?]]]
|
||||||
|
[:disabled {:optional true} :boolean]
|
||||||
|
[:dimmed {:optional true} :boolean]
|
||||||
[:aria-label {:optional true} :string]])
|
[:aria-label {:optional true} :string]])
|
||||||
|
|
||||||
(def ^:private schema:options-dropdown
|
(def ^:private schema:options-dropdown
|
||||||
|
|||||||
@ -59,9 +59,11 @@
|
|||||||
:key (weak-key option)
|
:key (weak-key option)
|
||||||
:id id
|
:id id
|
||||||
:label (get option :label)
|
:label (get option :label)
|
||||||
|
:title (get option :title)
|
||||||
:aria-label (get option :aria-label)
|
:aria-label (get option :aria-label)
|
||||||
:icon (get option :icon)
|
:icon (get option :icon)
|
||||||
:avatar (get option :avatar)
|
:avatar (get option :avatar)
|
||||||
|
:disabled (true? (:disabled option))
|
||||||
:ref ref
|
:ref ref
|
||||||
:role "option"
|
:role "option"
|
||||||
:focused (= id focused)
|
:focused (= id focused)
|
||||||
|
|||||||
@ -1150,6 +1150,9 @@ msgstr "This team is not part of any organization"
|
|||||||
msgid "dashboard.team-organization.add"
|
msgid "dashboard.team-organization.add"
|
||||||
msgstr "Add to an organization"
|
msgstr "Add to an organization"
|
||||||
|
|
||||||
|
msgid "dashboard.team-organization.add.no-valid-orgs"
|
||||||
|
msgstr "You are not allowed to add this team to any of your organizations."
|
||||||
|
|
||||||
msgid "dashboard.select-org-modal.title"
|
msgid "dashboard.select-org-modal.title"
|
||||||
msgstr "Add team to an organization"
|
msgstr "Add team to an organization"
|
||||||
|
|
||||||
@ -1183,6 +1186,9 @@ msgstr "Change team organization"
|
|||||||
msgid "dashboard.team-organization.remove"
|
msgid "dashboard.team-organization.remove"
|
||||||
msgstr "Remove team from organization"
|
msgstr "Remove team from organization"
|
||||||
|
|
||||||
|
msgid "dashboard.team-organization.disabled-org-tooltip"
|
||||||
|
msgstr "Only people within your organization can be invited."
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/team.cljs:1344
|
#: src/app/main/ui/dashboard/team.cljs:1344
|
||||||
msgid "dashboard.team-projects"
|
msgid "dashboard.team-projects"
|
||||||
msgstr "Team projects"
|
msgstr "Team projects"
|
||||||
|
|||||||
@ -1154,6 +1154,9 @@ msgstr "Este equipo no pertenece a ninguna organización"
|
|||||||
msgid "dashboard.team-organization.add"
|
msgid "dashboard.team-organization.add"
|
||||||
msgstr "Añadir a una organización"
|
msgstr "Añadir a una organización"
|
||||||
|
|
||||||
|
msgid "dashboard.team-organization.add.no-valid-orgs"
|
||||||
|
msgstr "No tienes permiso para añadir este equipo a ninguna de tus organizaciones."
|
||||||
|
|
||||||
msgid "dashboard.select-org-modal.title"
|
msgid "dashboard.select-org-modal.title"
|
||||||
msgstr "Añadir el equipo a una organización"
|
msgstr "Añadir el equipo a una organización"
|
||||||
|
|
||||||
@ -1187,6 +1190,9 @@ msgstr "Cambiar el equipo de organización"
|
|||||||
msgid "dashboard.team-organization.remove"
|
msgid "dashboard.team-organization.remove"
|
||||||
msgstr "Eliminar equipo de la organización"
|
msgstr "Eliminar equipo de la organización"
|
||||||
|
|
||||||
|
msgid "dashboard.team-organization.disabled-org-tooltip"
|
||||||
|
msgstr "Solo se puede invitar a personas que estén dentro de tu organización."
|
||||||
|
|
||||||
#: src/app/main/ui/dashboard/team.cljs:1344
|
#: src/app/main/ui/dashboard/team.cljs:1344
|
||||||
msgid "dashboard.team-projects"
|
msgid "dashboard.team-projects"
|
||||||
msgstr "Proyectos del equipo"
|
msgstr "Proyectos del equipo"
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user