🐛 Fix organization/team switcher issues from UX review (#11899)

* 🐛 Fix organization/team switcher issues from UX review

* 🐛 Let members leave an organization without SSO credentials

* 🐛 Fix style from previewed organization in the team switcher

* 🐛 Fix leave-organization modal test stubs
This commit is contained in:
María Valderrama 2026-09-25 09:54:58 +02:00 committed by GitHub
parent dfd28b1e57
commit f35e12f716
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 201 additions and 140 deletions

View File

@ -143,6 +143,7 @@
(def ^:private sql:get-member-teams-info
"SELECT t.id,
t.name,
t.is_default,
tpr.is_owner,
(SELECT count(*) FROM team_profile_rel WHERE team_id = t.id) AS num_members,
@ -214,6 +215,19 @@
[:teams-to-transfer ::sm/int]
[:teams-to-exit ::sm/int]
[:teams-to-detach ::sm/int]
[:team-ids-to-delete [:vector ::sm/uuid]]
[:transferable-teams
[:vector
[:map
[:id ::sm/uuid]
[:name ::sm/text]
[:members
[:vector
[:map
[:id ::sm/uuid]
[:name :string]
[:email ::sm/text]
[:is-admin ::sm/boolean]]]]]]]
[:member-added-at [:maybe ct/schema:inst]]
[:organization-member-count-before ::sm/int]])
@ -361,6 +375,7 @@
(sv/defmethod ::leave-organization
{::rpc/auth true
::nitrate/sso false
::doc/added "2.18"
::sm/params schema:leave-organization
::db/transaction true}
@ -371,8 +386,21 @@
:deleted-by-role "organization-member")))
(defn- get-transferable-teams
"`teams-to-transfer` with their members, except `profile-id`."
[{:keys [::db/conn]} teams-to-transfer profile-id]
(mapv (fn [{:keys [id name]}]
{:id id
:name name
:members (->> (teams/get-team-members conn id)
(remove #(= profile-id (:id %)))
(mapv #(select-keys % [:id :name :email :is-admin])))})
teams-to-transfer))
;; Not SSO gated, so members can leave without SSO credentials.
(sv/defmethod ::get-leave-organization-summary
{::rpc/auth true
::nitrate/sso false
::doc/added "2.18"
::sm/params schema:get-leave-organization-summary
::sm/result schema:get-leave-organization-summary-result
@ -394,6 +422,8 @@
:code :not-valid-teams))
(assoc
(get-leave-organization-summary cfg default-team-id valid-teams-to-delete-ids teams-to-transfer-count teams-to-exit-count)
:team-ids-to-delete (vec valid-teams-to-delete-ids)
:transferable-teams (get-transferable-teams cfg valid-teams-to-transfer profile-id)
:member-added-at (:created-at membership)
:organization-member-count-before (count organization-members))))

View File

@ -584,6 +584,8 @@
:teams-to-transfer 0
:teams-to-exit 0
:teams-to-detach 0
:team-ids-to-delete []
:transferable-teams []
:member-added-at (ct/inst "2026-07-17T12:00:00Z")
:organization-member-count-before 2}
(:result out)))))))
@ -618,10 +620,50 @@
:teams-to-transfer 0
:teams-to-exit 0
:teams-to-detach 1
:team-ids-to-delete [(:id extra-team)]
:transferable-teams []
:member-added-at (ct/inst "2026-07-17T12:00:00Z")
:organization-member-count-before 2}
(:result out)))))))
(t/deftest get-leave-organization-summary-lists-transferable-teams-with-members
(let [profile-owner (th/create-profile* 1 {:is-active true})
profile-user (th/create-profile* 2 {:is-active true})
;; profile-user owns both; only pair-team has another member
solo-team (th/create-team* 1 {:profile-id (:id profile-user)})
pair-team (th/create-team* 2 {:profile-id (:id profile-user)})
_ (th/create-team-role* {:team-id (:id pair-team)
:profile-id (:id profile-owner)
:role :admin})
organization-default-team (th/create-team* 99 {:profile-id (:id profile-user)})
organization-id (uuid/random)
your-penpot-id (:id organization-default-team)
organization-summary (make-organization-summary
:organization-id organization-id
:organization-name "Test Org"
:owner-id (:id profile-owner)
:your-penpot-teams [your-penpot-id]
:organization-teams [(:id solo-team) (:id pair-team)])]
(with-redefs [nitrate/call (nitrate-organization-summary-only-mock organization-summary)]
(let [out (th/command! {::th/type :get-leave-organization-summary
::rpc/profile-id (:id profile-user)
:id organization-id
:default-team-id your-penpot-id})
result (:result out)]
(t/is (th/success? out))
(t/is (= 1 (:teams-to-transfer result)))
(t/is (= [(:id solo-team)] (:team-ids-to-delete result)))
;; The members that can take over pair-team, without profile-user
(t/is (= [{:id (:id pair-team)
:name (:name pair-team)
:members [{:id (:id profile-owner)
:name (:fullname profile-owner)
:email (:email profile-owner)
:is-admin true}]}]
(:transferable-teams result)))))))
(t/deftest leave-organization-error-organization-owner-cannot-leave
(let [profile-owner (th/create-profile* 1 {:is-active true})
organization-default-team (th/create-team* 99 {:profile-id (:id profile-owner)})

View File

@ -177,22 +177,13 @@
(filter #(= (dm/get-in % [:organization :id]) organization-id))))
(defn organization-leave-info
"Splits the teams of an organization into what is needed to leave it:
the organization's own default team id, the teams owned by the
current user (whose membership decides whether they get deleted or
offered for transfer), and the teams the user does not own (which
are simply left)."
"The default team id and the not-owned teams of an organization.
Owned teams come from `::get-leave-organization-summary`."
[org-teams]
(let [non-default-teams (remove :is-default org-teams)]
{:default-team-id (->> org-teams (filter :is-default) first :id)
:owned-teams (filter #(dm/get-in % [:permissions :is-owner]) non-default-teams)
:not-owned-teams (remove #(dm/get-in % [:permissions :is-owner]) non-default-teams)}))
(defn transferable-teams
"Owned teams with more than one member: the ones the user can offer
to transfer to another owner instead of leaving/deleting them."
[owned-teams]
(filter #(> (count (:members %)) 1) owned-teams))
{:default-team-id (->> org-teams (filter :is-default) first :id)
:not-owned-teams (->> org-teams
(remove :is-default)
(remove #(dm/get-in % [:permissions :is-owner])))})
(def ^:private team-leave-error-messages
{:only-owner-can-delete-team "errors.team-leave.only-owner-can-delete"
@ -268,21 +259,15 @@
(rx/catch on-error)))))))
(defn leave-organization-fn
"Builds the accept callback used by `show-leave-organization-modal`: it
folds any teams the user chose to transfer into `:teams-to-leave`,
computes `:teams-to-delete` from the owned teams left with a single
member, then emits `leave-organization`."
[{:keys [organization default-team-id owned-teams not-owned-teams on-error]}]
(fn [{:keys [teams-to-transfer member-added-at organization-member-count-before]}]
"Builds the accept callback used by `show-leave-organization-modal`:
folds transferred teams into `:teams-to-leave` and emits
`leave-organization`."
[{:keys [organization default-team-id not-owned-teams on-error]}]
(fn [{:keys [teams-to-transfer teams-to-delete member-added-at organization-member-count-before]}]
(let [teams-to-leave
(cond->> not-owned-teams
:always (map #(select-keys % [:id]))
(seq teams-to-transfer) (concat teams-to-transfer))
teams-to-delete
(->> owned-teams
(filter #(= (count (:members %)) 1))
(map :id))]
(seq teams-to-transfer) (concat teams-to-transfer))]
(st/emit! (leave-organization {:id (:id organization)
:name (:name organization)
:default-team-id default-team-id
@ -293,7 +278,7 @@
:on-error on-error})))))
(defn show-leave-organization-modal
[{:keys [organization profile default-team-id leave-fn teams-to-transfer on-error]}]
[{:keys [organization profile default-team-id leave-fn on-error]}]
(ptk/reify ::show-leave-organization-modal
ptk/WatchEvent
(watch [_ _ _]
@ -309,6 +294,7 @@
(fn [params]
(leave-fn
(assoc params
:teams-to-delete (:team-ids-to-delete summary)
:member-added-at (:member-added-at summary)
:organization-member-count-before
(:organization-member-count-before summary))))]
@ -318,7 +304,7 @@
(modal/show
{:type :leave-and-reassign-organization
:profile profile
:teams-to-transfer teams-to-transfer
:teams-to-transfer (:transferable-teams summary)
:num-teams-to-delete num-teams-to-delete
:accept leave-fn}))

View File

@ -56,6 +56,19 @@
[team]
(if (:is-default team) (tr "dashboard.personal-projects") (:name team)))
(defn show-subscription-badge?
"Whether `team`'s U/E subscription badge should render: never on a
default team (\"Personal projects\" has no billing of its own), and
never on a team that belongs to an organization, since a team's
`:subscription` reflects its owner's own plan, not any
organization-level billing. Pending a real editor/seat billing
model for organizations, only a standalone team outside every
organization shows its own plan."
[team]
(and (not (:is-default team))
(nil? (dm/get-in team [:organization :id]))
(contains? #{"unlimited" "enterprise"} (get-subscription-type (:subscription team)))))
(defn sort-organization-teams
"Orders the teams of a single organization for the dropdown's second
column: alphabetical, with the organization's default team
@ -117,8 +130,9 @@
(defn create-team-target-id
"The organization default team id that \"Create new team\" targets,
or nil when the new team belongs under \"Other teams\". Creation
always targets the open dashboard's organization, not the one
previewed in the switcher."
targets the organization currently previewed in the switcher's left
column, i.e. the one the user triggers the action from, not
necessarily the open dashboard's own organization."
[organizations organization-id]
(when-not (= organization-id personal-bucket-id)
(:default-team-id (get organizations organization-id))))
@ -159,7 +173,7 @@
(mf/defc organizations-column*
{::mf/private true}
[{:keys [organizations selected-id ^boolean has-organizations? on-select on-create-organization
[{:keys [organizations selected-id current-id ^boolean has-organizations? on-select on-create-organization
admin-console-href ^boolean valid-license on-context-menu on-dismiss-context-menu]}]
[:li {:role "presentation" :class (stl/css :organizations-column)}
[:div {:class (stl/css :column-label)}
@ -193,7 +207,7 @@
[:span {:class (stl/css :organization-text)
:title (if personal? (tr "dashboard.other-teams") (:name organization))}
(if personal? (tr "dashboard.other-teams") (:name organization))]
(when (= bucket-id selected-id)
(when (= bucket-id current-id)
[:span {:class (stl/css :tick-icon)}
[:> icon* {:icon-id i/tick :size "s"}]])]
[:span {:class (stl/css :chevron-icon)}
@ -236,7 +250,8 @@
[:ul {:class (stl/css :column-list)}
(for [team teams]
(let [subscription-type (get-subscription-type (:subscription team))]
(let [subscription-type (get-subscription-type (:subscription team))
show-badge? (show-subscription-badge? team)]
[:* {:key (str (:id team))}
;; "Personal projects" is always last (see
;; `sort-organization-teams`/`sort-all-teams`), set apart
@ -264,7 +279,7 @@
[:span {:class (stl/css :team-text)
:title (team-display-name team)}
(team-display-name team)]
(when (#{"unlimited" "enterprise"} subscription-type)
(when show-badge?
[:> menu-team-icon* {:subscription-type subscription-type}])
(when (= (:id team) selected-team-id)
[:span {:class (stl/css :tick-icon)}
@ -460,17 +475,18 @@
(mf/with-memo [selected-organization profile]
(resolve-admin-console-href selected-organization profile))
;; "Create new team" targets the open dashboard's organization
;; (`current-organization-id`), not the one previewed on the
;; left column; the story keeps this behaviour unchanged.
;; "Create new team" targets the organization previewed on the
;; left column (`selected-organization-id`), i.e. wherever the
;; user triggers the action from, or "Other teams" when
;; that's the previewed group.
on-create-team
(mf/use-fn
(mf/deps organizations current-organization-id)
(mf/deps organizations selected-organization-id)
(fn []
(reset! show-menu* false)
(if (contains? cf/flags :admin-console)
(st/emit! (dtm/check-and-create-team
(create-team-target-id organizations current-organization-id)))
(create-team-target-id organizations selected-organization-id)))
(st/emit! (modal/show :team-form {})))))
on-close-context-menu
@ -531,7 +547,7 @@
[:span {:class (stl/css :current-team-name)
:title (team-display-name team)}
(team-display-name team)]
(when (#{"unlimited" "enterprise"} current-team-subscription-type)
(when (show-subscription-badge? team)
[:> menu-team-icon* {:subscription-type current-team-subscription-type}])]
(when-let [organization-name (closed-control-line-2 has-organizations? current-organization)]
[:span {:class (stl/css :current-organization-name)
@ -565,6 +581,7 @@
:class (stl/css :organization-team-dropdown)}
[:> organizations-column* {:organizations (sort-organizations (vals organizations))
:selected-id selected-organization-id
:current-id current-organization-id
:has-organizations? has-organizations?
:on-select on-organization-select
:on-create-organization on-create-organization

View File

@ -24,33 +24,10 @@
[beicon.v2.core :as rx]
[rumext.v2 :as mf]))
(defn- fetch-missing-team-members!
"Fetches member lists for any `owned-teams` that do not have them
loaded yet; shared by the team-options menu and the organization
context menu, both of which need `:members` to decide how a team
leave/delete should behave."
[owned-teams]
(doseq [owned-team owned-teams
:when (not (contains? owned-team :members))]
(st/emit! (dtm/fetch-members (:id owned-team)))))
(defn- use-organization-leave
"Data needed to leave `organization`, out of its `teams`: the
organization's own default team id, the teams owned by the current
user (`:owned-teams`) split from the ones they don't own
(`:not-owned-teams`), which owned teams can be offered for transfer
(`:teams-to-transfer`), and the ready-to-call `:leave-fn` that
`show-leave-organization-modal` expects as its accept callback.
Shared by `options-dropdown*` and `organization-context-menu*`, the
two places that offer a \"leave organization\" action. Also fetches
member lists for any owned team that doesn't have them loaded yet,
since leaving needs `:members` to decide delete vs. transfer.
`enabled?` must stay false whenever leaving isn't actually offered
(e.g. `options-dropdown*` when the profile can't leave the
organization): called unconditionally like any hook, but it skips
the teams/members lookup and the member-fetching effect, so no
RPC calls are made for an action that's not on offer."
"The default team id and `:leave-fn` needed to leave `organization`.
Fetches nothing: fetching team members would hit the SSO gate. Pass
`enabled?` false when leaving isn't offered."
[organization teams ^boolean enabled?]
(let [org-teams
(mf/with-memo [teams organization enabled?]
@ -58,36 +35,20 @@
(dnt/organization-teams teams (:id organization))))
{default-team-id :default-team-id
owned-teams :owned-teams
not-owned-teams :not-owned-teams}
(mf/with-memo [org-teams]
(when org-teams
(dnt/organization-leave-info org-teams)))
teams-to-transfer
(mf/with-memo [owned-teams]
(when owned-teams
(dnt/transferable-teams owned-teams)))
leave-fn
(mf/use-fn
(mf/deps organization default-team-id owned-teams not-owned-teams)
(mf/deps organization default-team-id not-owned-teams)
(dnt/leave-organization-fn {:organization organization
:default-team-id default-team-id
:owned-teams owned-teams
:not-owned-teams not-owned-teams
:on-error dnt/org-leave-on-error}))]
(mf/use-effect
(mf/deps owned-teams)
(fn []
(when (seq owned-teams)
(fetch-missing-team-members! owned-teams))))
{:default-team-id default-team-id
:owned-teams owned-teams
:not-owned-teams not-owned-teams
:teams-to-transfer teams-to-transfer
:leave-fn leave-fn}))
(mf/defc options-dropdown*
@ -108,14 +69,9 @@
(not (:is-default team))
{org-default-team-id :default-team-id
org-owned-teams :owned-teams
teams-to-transfer :teams-to-transfer
org-leave-fn :leave-fn}
(use-organization-leave current-organization teams can-leave-organization)
owned-teams-members-loaded?
(every? #(contains? % :members) org-owned-teams)
on-success
(fn []
(rx/of (dcm/go-to-dashboard-recent :team-id default-team-id)
@ -203,18 +159,15 @@
on-leave-organization-clicked
(mf/use-fn
(mf/deps profile current-organization org-default-team-id teams-to-transfer
org-leave-fn owned-teams-members-loaded?)
(mf/deps profile current-organization org-default-team-id org-leave-fn)
(fn []
(when owned-teams-members-loaded?
(on-close)
(st/emit! (dnt/show-leave-organization-modal
{:organization current-organization
:profile profile
:default-team-id org-default-team-id
:leave-fn org-leave-fn
:teams-to-transfer teams-to-transfer
:on-error dnt/org-leave-on-error})))))]
(on-close)
(st/emit! (dnt/show-leave-organization-modal
{:organization current-organization
:profile profile
:default-team-id org-default-team-id
:leave-fn org-leave-fn
:on-error dnt/org-leave-on-error}))))]
[:> dropdown-menu* {:show show
:on-close on-close
@ -284,37 +237,23 @@
(mf/defc organization-context-menu*
"Right-click menu on an organization in `organizations-column*`,
currently offering just \"leave organization\". Split out of
`organization-team-switch*` so its leave-organization data (owned
teams, transfer candidates, member loading) stays local instead of
being computed unconditionally on every render of the switcher."
`organization-team-switch*` so its leave-organization data stays
local instead of being computed on every render of the switcher."
[{:keys [organization teams profile x y on-close on-leave-requested]}]
(let [{default-team-id :default-team-id
owned-teams :owned-teams
teams-to-transfer :teams-to-transfer
leave-fn :leave-fn}
(use-organization-leave organization teams true)
owned-teams-members-loaded?
(every? #(contains? % :members) owned-teams)
on-leave-clicked
(mf/use-fn
(mf/deps leave-fn
profile
organization
default-team-id
teams-to-transfer
owned-teams-members-loaded?
on-leave-requested)
(mf/deps leave-fn profile organization default-team-id on-leave-requested)
(fn []
(when owned-teams-members-loaded?
(on-leave-requested)
(st/emit! (dnt/show-leave-organization-modal {:organization organization
:profile profile
:default-team-id default-team-id
:leave-fn leave-fn
:teams-to-transfer teams-to-transfer
:on-error dnt/org-leave-on-error})))))]
(on-leave-requested)
(st/emit! (dnt/show-leave-organization-modal {:organization organization
:profile profile
:default-team-id default-team-id
:leave-fn leave-fn
:on-error dnt/org-leave-on-error}))))]
[:> dropdown-menu* {:show true
:on-close on-close

View File

@ -14,10 +14,12 @@
[app.main.data.nitrate-audit :as nitrate-audit]
[app.main.data.notifications :as ntf]
[app.main.data.team :as dt]
[app.main.repo :as rp]
[app.main.store :as st]
[app.main.ui.auth.verify-token :as verify-token]
[beicon.v2.core :as rx]
[cljs.test :as t :include-macros true]
[frontend-tests.helpers.mock :as mock]
[potok.v2.core :as ptk]))
(t/deftest account-age-days-test
@ -270,31 +272,20 @@
(t/is (= ["t1" "t4"] (map :id (dnt/organization-teams teams "org-a"))))
(t/is (= [] (dnt/organization-teams teams "org-c")))))
(t/deftest organization-leave-info-splits-owned-and-not-owned-teams
(t/deftest organization-leave-info-keeps-default-and-not-owned-teams
(let [org-teams [{:id "default" :is-default true}
{:id "owned-1" :permissions {:is-owner true}}
{:id "owned-2" :permissions {:is-owner true}}
{:id "member-1" :permissions {:is-owner false}}]
info (dnt/organization-leave-info org-teams)]
(t/is (= "default" (:default-team-id info)))
(t/is (= ["owned-1" "owned-2"] (map :id (:owned-teams info))))
(t/is (= ["member-1"] (map :id (:not-owned-teams info))))))
(t/deftest transferable-teams-boundary-at-one-member
(let [owned-teams [{:id "solo" :members [{:id "m1"}]}
{:id "pair" :members [{:id "m1"} {:id "m2"}]}
{:id "empty" :members []}]]
(t/is (= ["pair"] (map :id (dnt/transferable-teams owned-teams))))))
(t/deftest leave-organization-fn-builds-delete-and-leave-lists
(let [captured (atom nil)
emitted (atom [])
owned-teams [{:id "solo" :members [{:id "m1"}]}
{:id "pair" :members [{:id "m1"} {:id "m2"}]}]
not-owned-teams [{:id "member-1" :name "extra"}]
leave-fn (dnt/leave-organization-fn {:organization {:id "org-1" :name "Acme"}
:default-team-id "default"
:owned-teams owned-teams
:not-owned-teams not-owned-teams
:on-error :on-error-fn})]
(with-redefs [dnt/leave-organization (fn [params] (reset! captured params) ::leave-event)
@ -304,6 +295,7 @@
(t/testing "with no teams offered for transfer"
(leave-fn {:teams-to-transfer nil
:teams-to-delete ["solo"]
:member-added-at "2026-07-17T00:00:00Z"
:organization-member-count-before 3})
@ -327,6 +319,45 @@
{:id "member-1"}]
(:teams-to-leave @captured)))))))
(t/deftest show-leave-organization-modal-takes-owned-teams-from-the-summary
(let [requests (atom [])
accepted (atom nil)
transferable [{:id "pair" :name "Pair" :members [{:id "m2" :name "Other"}]}]
summary {:teams-to-delete 1
:teams-to-transfer 1
:teams-to-exit 0
:teams-to-detach 0
:team-ids-to-delete ["solo"]
:transferable-teams transferable
:member-added-at "2026-07-17T00:00:00Z"
:organization-member-count-before 3}
event (dnt/show-leave-organization-modal
{:organization {:id "org-1" :name "Acme"}
:profile {:id "me"}
:default-team-id "default"
:leave-fn #(reset! accepted %)
:on-error identity})]
(with-redefs [rp/cmd! (mock/stub
(fn [cmd params]
(swap! requests conj cmd)
(t/is (= {:id "org-1" :default-team-id "default"} params))
(rx/of summary)))
modal/show (mock/stub (fn [params] params))]
(let [modal-params (atom nil)]
(rx/sub! (ptk/watch event nil nil) #(reset! modal-params %))
;; The summary is the only request: no SSO gated member fetch
(t/is (= [::dnt/get-leave-organization-summary] @requests))
(t/is (= :leave-and-reassign-organization (:type @modal-params)))
(t/is (= transferable (:teams-to-transfer @modal-params)))
((:accept @modal-params) {:teams-to-transfer [{:id "pair" :reassign-to "m2"}]})
(t/is (= {:teams-to-transfer [{:id "pair" :reassign-to "m2"}]
:teams-to-delete ["solo"]
:member-added-at "2026-07-17T00:00:00Z"
:organization-member-count-before 3}
@accepted))))))
(t/deftest team-leave-on-error-matrix
(t/testing "known error code shows a translated notification"
(let [emitted (atom [])]

View File

@ -57,16 +57,16 @@
(t/testing "profile with no organizations at all: line absent"
(t/is (nil? (dts/closed-control-line-2 false nil)))))
(t/deftest create-team-targets-the-current-organization
(t/deftest create-team-targets-the-previewed-organization
(let [organizations {"org-a" {:id "org-a" :name "Org A" :default-team-id "team-a-default"}
"org-b" {:id "org-b" :name "Org B" :default-team-id "team-b-default"}
nil nil}
other-teams-id (dts/organization-bucket-id nil)]
(t/testing "target is the open dashboard organization's default team"
(t/testing "target is the previewed organization's default team"
(t/is (= "team-a-default" (dts/create-team-target-id organizations "org-a")))
(t/is (= "team-b-default" (dts/create-team-target-id organizations "org-b"))))
(t/testing "current destination is Other teams: no organization target"
(t/testing "previewed group is Other teams: no organization target"
(t/is (nil? (dts/create-team-target-id organizations other-teams-id))))))
(t/deftest teams-for-organization-filters-and-sorts-per-organization
@ -117,6 +117,22 @@
(t/testing "flag absent: fallback action hidden"
(t/is (false? (dts/show-create-organization-in-teams-column? #{})))))
(t/deftest show-subscription-badge-matrix
(t/testing "default team: never, regardless of its own subscription"
(t/is (false? (dts/show-subscription-badge? {:is-default true
:subscription {:type "unlimited"}}))))
(t/testing "team in an organization: never, regardless of its own subscription"
(t/is (false? (dts/show-subscription-badge? {:organization {:id "org-a"}
:subscription {:type "unlimited"}}))))
(t/testing "standalone team outside every organization: shown for unlimited/enterprise"
(t/is (true? (dts/show-subscription-badge? {:subscription {:type "unlimited"}})))
(t/is (true? (dts/show-subscription-badge? {:subscription {:type "enterprise"}}))))
(t/testing "standalone team with a professional plan: not shown"
(t/is (false? (dts/show-subscription-badge? {:subscription {:type "professional"}})))))
(t/deftest admin-console-href-resolution-matrix
(t/testing "owner of the previewed organization: organization-specific href"
(let [organization {:id "org-a" :slug "org-a-slug" :owner-id "profile-1"}

View File

@ -10931,4 +10931,4 @@ msgid "labels.team-management"
msgstr "Team management"
msgid "dashboard.no-organizations-yet"
msgstr "You don't belong to any organization yet"
msgstr "No organizations yet."

View File

@ -10563,4 +10563,4 @@ msgid "labels.team-management"
msgstr "Gestión de equipos"
msgid "dashboard.no-organizations-yet"
msgstr "Todavía no perteneces a ninguna organización"
msgstr "Todavía no hay organizaciones."