diff --git a/backend/src/app/rpc/commands/nitrate.clj b/backend/src/app/rpc/commands/nitrate.clj index 87b9409f27..0a51654c3a 100644 --- a/backend/src/app/rpc/commands/nitrate.clj +++ b/backend/src/app/rpc/commands/nitrate.clj @@ -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)))) diff --git a/backend/test/backend_tests/rpc_nitrate_test.clj b/backend/test/backend_tests/rpc_nitrate_test.clj index fd64185a2a..978a91ca19 100644 --- a/backend/test/backend_tests/rpc_nitrate_test.clj +++ b/backend/test/backend_tests/rpc_nitrate_test.clj @@ -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)}) diff --git a/frontend/src/app/main/data/nitrate.cljs b/frontend/src/app/main/data/nitrate.cljs index d8bbc8be2e..80c337481c 100644 --- a/frontend/src/app/main/data/nitrate.cljs +++ b/frontend/src/app/main/data/nitrate.cljs @@ -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})) diff --git a/frontend/src/app/main/ui/dashboard/organization_team_switch.cljs b/frontend/src/app/main/ui/dashboard/organization_team_switch.cljs index 2ee01b4ad9..16ec74dd84 100644 --- a/frontend/src/app/main/ui/dashboard/organization_team_switch.cljs +++ b/frontend/src/app/main/ui/dashboard/organization_team_switch.cljs @@ -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 diff --git a/frontend/src/app/main/ui/dashboard/organization_team_switch_menus.cljs b/frontend/src/app/main/ui/dashboard/organization_team_switch_menus.cljs index ca28c6aa21..2124f5d673 100644 --- a/frontend/src/app/main/ui/dashboard/organization_team_switch_menus.cljs +++ b/frontend/src/app/main/ui/dashboard/organization_team_switch_menus.cljs @@ -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 diff --git a/frontend/test/frontend_tests/data/nitrate_test.cljs b/frontend/test/frontend_tests/data/nitrate_test.cljs index eb50f0b0c9..3937fb7afa 100644 --- a/frontend/test/frontend_tests/data/nitrate_test.cljs +++ b/frontend/test/frontend_tests/data/nitrate_test.cljs @@ -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 [])] diff --git a/frontend/test/frontend_tests/ui/organization_team_switch_test.cljs b/frontend/test/frontend_tests/ui/organization_team_switch_test.cljs index 7deca67011..6cf00fa228 100644 --- a/frontend/test/frontend_tests/ui/organization_team_switch_test.cljs +++ b/frontend/test/frontend_tests/ui/organization_team_switch_test.cljs @@ -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"} diff --git a/frontend/translations/en.po b/frontend/translations/en.po index 7a0bf13760..79c4283e26 100644 --- a/frontend/translations/en.po +++ b/frontend/translations/en.po @@ -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." diff --git a/frontend/translations/es.po b/frontend/translations/es.po index bb9873cd50..016ee661ab 100644 --- a/frontend/translations/es.po +++ b/frontend/translations/es.po @@ -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."