From c2a975b3f73a7bd1d4c5c9f0828702e5dcad0d59 Mon Sep 17 00:00:00 2001 From: Juan Castro Date: Fri, 11 Sep 2026 10:20:09 -0400 Subject: [PATCH] refactor: drop the team plan card It described a team-wide tier, which stopped existing when plans moved to the account. What it still showed -- what each tier costs -- is in the Change plan picker, next to the account it applies to, so nothing is lost by removing it. `teamPlan.js` and its tests go with it, along with twelve i18n keys nothing reads any more. `state.plan` stays: the Plan column and the picker both need the catalogue and the seat assignments. --- src/gui/src/UI/Dashboard/TabTeams.js | 7 -- src/gui/src/UI/Dashboard/teamPlan.js | 67 ------------------ src/gui/src/UI/Dashboard/teamPlan.test.js | 86 ----------------------- src/gui/src/i18n/translations/en.js | 12 ---- 4 files changed, 172 deletions(-) delete mode 100644 src/gui/src/UI/Dashboard/teamPlan.js delete mode 100644 src/gui/src/UI/Dashboard/teamPlan.test.js diff --git a/src/gui/src/UI/Dashboard/TabTeams.js b/src/gui/src/UI/Dashboard/TabTeams.js index c31736215..6946eff25 100644 --- a/src/gui/src/UI/Dashboard/TabTeams.js +++ b/src/gui/src/UI/Dashboard/TabTeams.js @@ -19,7 +19,6 @@ import UIAlert from '../UIAlert.js'; import UIPrompt from '../UIPrompt.js'; -import teamPlanHtml from './teamPlan.js'; import teamActionButton from './teamActionIcons.js'; import { annotateMembers, @@ -216,11 +215,6 @@ const renderMemberView = () => { return h + renderAudit(); }; -const renderPlan = () => teamPlanHtml({ - plan: state.plan, - canBuy: window.team_billing_ui === true, -}); - const renderDirectory = () => { const on = state.selected?.directoryEnabled === true; let h = '
'; @@ -244,7 +238,6 @@ const renderOwnerView = () => { h += ``; h += '
'; - h += renderPlan(); h += renderDirectory(); h += renderAddAccount(); h += renderMembers(); diff --git a/src/gui/src/UI/Dashboard/teamPlan.js b/src/gui/src/UI/Dashboard/teamPlan.js deleted file mode 100644 index ea671aac7..000000000 --- a/src/gui/src/UI/Dashboard/teamPlan.js +++ /dev/null @@ -1,67 +0,0 @@ -/* - * Copyright (C) 2024-present Puter Technologies Inc. - * - * This file is part of Puter. - * - * Puter is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published - * by the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -/** - * The team's plan card, drawn from whatever catalogue the server returned. No - * tier, price or payment provider is known here. - * - * @param {object} args `{ plan, seats, canBuy }` - * @returns {string} markup, or '' when there is no catalogue - */ -export const teamPlanHtml = ({ plan, canBuy = false } = {}) => { - if ( plan?.status !== 'ready' ) return ''; - const offerings = Array.isArray(plan.offerings) ? plan.offerings : []; - const quantities = plan.tierQuantities ?? {}; - const onSomething = Object.values(quantities).some(n => n > 0); - - let h = '
'; - h += `

${i18n('teams_plan')}

`; - h += `

${i18n( - onSomething ? 'teams_plan_per_account_hint' : 'teams_plan_none', - )}

`; - if ( plan.subStatus && plan.subStatus !== 'active' ) { - h += `

${window.html_encode(plan.subStatus)}

`; - } - - h += '
    '; - for ( const o of offerings ) { - const count = quantities[o.tier] ?? 0; - h += '
  • '; - h += `${window.html_encode(o.name_en || o.tier)}`; - h += `${i18n('teams_plan_per_seat', { - amount: o.amountPerSeat, - currency: o.currency, - })}`; - if ( count > 0 ) { - h += `${i18n('teams_plan_on_count', { count })}`; - } else if ( ! o.available ) { - // The server says no price is configured; buying would 422. - h += `${i18n('teams_plan_unavailable')}`; - } - h += '
  • '; - } - h += '
'; - if ( canBuy ) { - h += `

${i18n('teams_plan_assign_hint')}

`; - } - h += '
'; - return h; -}; - -export default teamPlanHtml; diff --git a/src/gui/src/UI/Dashboard/teamPlan.test.js b/src/gui/src/UI/Dashboard/teamPlan.test.js deleted file mode 100644 index 2b4fbf14a..000000000 --- a/src/gui/src/UI/Dashboard/teamPlan.test.js +++ /dev/null @@ -1,86 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -globalThis.i18n = (key, args) => - args && !Array.isArray(args) - ? `${key}(${Object.entries(args).map(([k, v]) => `${k}=${v}`).join(',')})` - : key; -globalThis.window = { html_encode: (v) => String(v).replace(/ ({ - itemId: 'puter-team-basic', - tier: 'team-basic', - name_en: 'Team Basic', - amountPerSeat: 10, - currency: 'USD', - available: true, - ...over, -}); - -describe('the team plan card', () => { - const ready = (over = {}) => ({ - status: 'ready', - offerings: [offering()], - tierQuantities: {}, - ...over, - }); - - it('draws nothing when no catalogue came back', () => { - for (const plan of [null, undefined, { status: 'unavailable' }, {}]) { - expect(teamPlanHtml({ plan })).toBe(''); - } - }); - - it('says plans are per account once something is bought', () => { - const h = teamPlanHtml({ - plan: ready({ tierQuantities: { 'team-basic': 2 } }), - }); - expect(h).toContain('teams_plan_per_account_hint'); - }); - - it('says nothing is bought when no tier has a seat', () => { - expect(teamPlanHtml({ plan: ready() })).toContain('teams_plan_none'); - }); - - it('shows how many accounts are on each tier', () => { - const h = teamPlanHtml({ - plan: ready({ tierQuantities: { 'team-basic': 3 } }), - }); - expect(h).toContain('count=3'); - }); - - it('marks a tier with no configured price unavailable', () => { - // Buying one 422s, so it must not look purchasable. - const h = teamPlanHtml({ - plan: ready({ offerings: [offering({ available: false })] }), - }); - expect(h).toContain('teams_plan_unavailable'); - }); - - it('offers no assignment hint without a billing extension', () => { - const h = teamPlanHtml({ plan: ready(), canBuy: false }); - expect(h).not.toContain('teams_plan_assign_hint'); - // Prices still render; only the action is missing. - expect(h).toContain('Team Basic'); - }); - - it('points at the table when a billing extension is present', () => { - // The card is a summary now; the chooser is per row. - const h = teamPlanHtml({ plan: ready(), canBuy: true }); - expect(h).toContain('teams_plan_assign_hint'); - expect(h).not.toContain('teams-plan-buy'); - }); - - it('surfaces a status that is not active, so dunning is visible', () => { - const h = teamPlanHtml({ plan: ready({ subStatus: 'past_due' }) }); - expect(h).toContain('past_due'); - }); - - it('encodes a name the server supplied', () => { - const h = teamPlanHtml({ - plan: ready({ offerings: [offering({ name_en: '' })] }), - }); - expect(h).not.toContain('