mirror of
https://github.com/HeyPuter/puter.git
synced 2026-09-28 16:17:05 +00:00
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:
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
@@ -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, '<') };
|
||||
|
||||
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>');
|
||||
});
|
||||
});
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user