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.
This commit is contained in:
Juan Castro
2026-09-11 10:20:09 -04:00
parent b8b0650990
commit c2a975b3f7
4 changed files with 0 additions and 172 deletions
-7
View File
@@ -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 = '<div class="dashboard-card teams-panel">';
@@ -244,7 +238,6 @@ const renderOwnerView = () => {
h += `<button class="button teams-rename">${i18n('teams_rename')}</button>`;
h += '</div>';
h += renderPlan();
h += renderDirectory();
h += renderAddAccount();
h += renderMembers();
-67
View File
@@ -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 <https://www.gnu.org/licenses/>.
*/
/**
* 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 = '<div class="dashboard-card teams-panel teams-plan">';
h += `<h3>${i18n('teams_plan')}</h3>`;
h += `<p class="teams-panel-hint">${i18n(
onSomething ? 'teams_plan_per_account_hint' : 'teams_plan_none',
)}</p>`;
if ( plan.subStatus && plan.subStatus !== 'active' ) {
h += `<p class="teams-plan-status">${window.html_encode(plan.subStatus)}</p>`;
}
h += '<ul class="teams-plan-list">';
for ( const o of offerings ) {
const count = quantities[o.tier] ?? 0;
h += '<li class="teams-plan-option">';
h += `<span class="teams-plan-name">${window.html_encode(o.name_en || o.tier)}</span>`;
h += `<span class="teams-plan-price">${i18n('teams_plan_per_seat', {
amount: o.amountPerSeat,
currency: o.currency,
})}</span>`;
if ( count > 0 ) {
h += `<span class="teams-plan-badge">${i18n('teams_plan_on_count', { count })}</span>`;
} else if ( ! o.available ) {
// The server says no price is configured; buying would 422.
h += `<span class="teams-plan-badge">${i18n('teams_plan_unavailable')}</span>`;
}
h += '</li>';
}
h += '</ul>';
if ( canBuy ) {
h += `<p class="teams-panel-hint">${i18n('teams_plan_assign_hint')}</p>`;
}
h += '</div>';
return h;
};
export default teamPlanHtml;
-86
View File
@@ -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(/</g, '&lt;') };
const { teamPlanHtml } = await import('./teamPlan.js');
const offering = (over = {}) => ({
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: '<script>x</script>' })] }),
});
expect(h).not.toContain('<script>');
});
});
-12
View File
@@ -529,24 +529,12 @@ const en = {
teams_accounts: 'Accounts',
teams_no_accounts: 'This team has no accounts yet.',
teams_account_of: 'This account belongs to %%',
teams_plan: 'Plan',
teams_plan_none: 'This team is on the free plan. Members get a reduced allowance.',
teams_plan_current: 'On {{plan}}, billed for {{seats}} accounts.',
teams_plan_current_one: 'On {{plan}}, billed for 1 account.',
teams_plan_current_badge: 'Current',
teams_plan_unavailable: 'Unavailable',
teams_plan_per_seat: '{{amount}} {{currency}} per account, per month',
teams_plan_buy: 'Choose',
teams_plan_switch: 'Switch',
teams_add_account: 'Add an account',
teams_email_optional: 'Email (optional)',
teams_add_account_email_hint: 'If you add an address, we email the username and temporary password to it. Otherwise the password below is the only copy.',
teams_add_account_hint:
'Puter creates the account and gives you a one-time password to pass on. The username has to be free across all of Puter.',
teams_plan_change_for: 'Which plan for {{username}}?',
teams_plan_per_account_hint: 'Plans are set per account, in the table below.',
teams_plan_on_count: '{{count}} on this plan',
teams_plan_assign_hint: 'Use Change plan on an account to put it on one of these.',
teams_plan_change: 'Change plan',
teams_member_plan: 'Plan',
teams_member_plan_payer: '— payer',