mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
feat(quota): all-groups default + stacked group sections + 3-col cards
- selectedGroupId defaults to "all" instead of "group-demo"
- <select> prepends <option value="all">{t("allGroups")}</option>
- Pool list replaced by groupsToRender map: one stacked section per group
(heading: group name + count via groupPools.length), each with a
grid-cols-1 md:grid-cols-2 xl:grid-cols-3 card grid
- Rename button guard changed from selectedGroupId !== "group-demo" to
selectedGroupId !== "all" (no single target when viewing all groups)
- i18n: allGroups added to en.json ("All groups") + pt-BR.json ("Todos os grupos")
- quota-share-layout-v2.test.ts: 10 source-scan + i18n parity assertions
- quota-groups-ui.test.ts: align group heading test to groupPools.length
This commit is contained in:
@@ -142,7 +142,7 @@ export default function QuotaSharePageClient() {
|
||||
|
||||
// ── Group state ───────────────────────────────────────────────────────────
|
||||
const [groups, setGroups] = useState<QuotaGroup[]>([]);
|
||||
const [selectedGroupId, setSelectedGroupId] = useState<string>("group-demo");
|
||||
const [selectedGroupId, setSelectedGroupId] = useState<string>("all");
|
||||
const [newGroupInput, setNewGroupInput] = useState("");
|
||||
const [showNewGroupInput, setShowNewGroupInput] = useState(false);
|
||||
const [renaming, setRenaming] = useState(false);
|
||||
@@ -286,12 +286,24 @@ export default function QuotaSharePageClient() {
|
||||
[pools, aggregate]
|
||||
);
|
||||
|
||||
// Pools filtered by selected group
|
||||
// Pools filtered by selected group (kept for stats/empty-state checks)
|
||||
const filteredPools = useMemo(
|
||||
() => pools.filter((p) => (p as unknown as { groupId?: string }).groupId === selectedGroupId || (!( p as unknown as { groupId?: string }).groupId && selectedGroupId === "group-demo")),
|
||||
() =>
|
||||
selectedGroupId === "all"
|
||||
? pools
|
||||
: pools.filter(
|
||||
(p) =>
|
||||
((p as unknown as { groupId?: string }).groupId ?? "group-demo") === selectedGroupId
|
||||
),
|
||||
[pools, selectedGroupId]
|
||||
);
|
||||
|
||||
// Groups to render as stacked sections
|
||||
const groupsToRender = useMemo(
|
||||
() => (selectedGroupId === "all" ? groups : groups.filter((g) => g.id === selectedGroupId)),
|
||||
[groups, selectedGroupId]
|
||||
);
|
||||
|
||||
// ── Mutations ─────────────────────────────────────────────────────────────
|
||||
|
||||
const handleSaveAllocations = useCallback(
|
||||
@@ -348,6 +360,7 @@ export default function QuotaSharePageClient() {
|
||||
title={t("groupSelectHint")}
|
||||
className="px-2 py-1 rounded border border-border bg-bg-base text-sm text-text-main min-w-[120px]"
|
||||
>
|
||||
<option value="all">{t("allGroups")}</option>
|
||||
{groups.map((g) => (
|
||||
<option key={g.id} value={g.id}>
|
||||
{g.name}
|
||||
@@ -394,7 +407,7 @@ export default function QuotaSharePageClient() {
|
||||
{t("newGroup")}
|
||||
</button>
|
||||
)}
|
||||
{selectedGroupId !== "group-demo" && (
|
||||
{selectedGroupId !== "all" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => void handleRenameGroup()}
|
||||
@@ -443,19 +456,7 @@ export default function QuotaSharePageClient() {
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Group heading */}
|
||||
{groups.find((g) => g.id === selectedGroupId) && (
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[16px] text-text-muted">folder</span>
|
||||
<span className="text-sm font-semibold text-text-main">
|
||||
{groups.find((g) => g.id === selectedGroupId)?.name}
|
||||
</span>
|
||||
<span className="text-[11px] text-text-muted">
|
||||
({filteredPools.length})
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{filteredPools.length === 0 ? (
|
||||
{groupsToRender.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-border bg-surface py-10 text-center">
|
||||
<p className="text-sm text-text-muted">{t("emptyDescription")}</p>
|
||||
<Button variant="primary" size="sm" className="mt-3" onClick={() => setCreateOpen(true)}>
|
||||
@@ -464,21 +465,58 @@ export default function QuotaSharePageClient() {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-3">
|
||||
{filteredPools.map((pool) => (
|
||||
<PoolCardWithUsage
|
||||
key={pool.id}
|
||||
pool={pool}
|
||||
keyLabels={keyLabels}
|
||||
connectionLabel={connLabel(pool.connectionId)}
|
||||
provider={connProvider(pool.connectionId)}
|
||||
providers={[...new Set((pool.connectionIds ?? [pool.connectionId]).map(connProvider))]}
|
||||
connectionIds={pool.connectionIds ?? [pool.connectionId]}
|
||||
onEdit={() => setEditing(pool)}
|
||||
onRemove={() => void handleRemovePool(pool.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
groupsToRender.map((g) => {
|
||||
const groupPools = pools.filter(
|
||||
(p) =>
|
||||
((p as unknown as { groupId?: string }).groupId ?? "group-demo") === g.id
|
||||
);
|
||||
return (
|
||||
<div key={g.id} className="flex flex-col gap-3">
|
||||
{/* Per-group heading */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="material-symbols-outlined text-[16px] text-text-muted">
|
||||
folder
|
||||
</span>
|
||||
<span className="text-sm font-semibold text-text-main">{g.name}</span>
|
||||
<span className="text-[11px] text-text-muted">({groupPools.length})</span>
|
||||
</div>
|
||||
{groupPools.length === 0 ? (
|
||||
<div className="rounded-xl border border-dashed border-border bg-surface py-6 text-center">
|
||||
<p className="text-sm text-text-muted">{t("emptyDescription")}</p>
|
||||
<Button
|
||||
variant="primary"
|
||||
size="sm"
|
||||
className="mt-3"
|
||||
onClick={() => setCreateOpen(true)}
|
||||
>
|
||||
<span className="material-symbols-outlined text-[14px] mr-1">add</span>
|
||||
{t("newPool")}
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-3">
|
||||
{groupPools.map((pool) => (
|
||||
<PoolCardWithUsage
|
||||
key={pool.id}
|
||||
pool={pool}
|
||||
keyLabels={keyLabels}
|
||||
connectionLabel={connLabel(pool.connectionId)}
|
||||
provider={connProvider(pool.connectionId)}
|
||||
providers={[
|
||||
...new Set(
|
||||
(pool.connectionIds ?? [pool.connectionId]).map(connProvider)
|
||||
),
|
||||
]}
|
||||
connectionIds={pool.connectionIds ?? [pool.connectionId]}
|
||||
onEdit={() => setEditing(pool)}
|
||||
onRemove={() => void handleRemovePool(pool.id)}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -7912,7 +7912,8 @@
|
||||
"groupSelectHint": "Filter pools by group",
|
||||
"groupAllocationNote": "Allocations apply to all pools in this group via the shared quota layer.",
|
||||
"groupNamePrompt": "Enter group name",
|
||||
"wizardGroupLabel": "Group"
|
||||
"wizardGroupLabel": "Group",
|
||||
"allGroups": "All groups"
|
||||
},
|
||||
"plugins": {
|
||||
"title": "Plugins",
|
||||
|
||||
@@ -5417,7 +5417,8 @@
|
||||
"groupSelectHint": "Filtrar pools por grupo",
|
||||
"groupAllocationNote": "As alocações se aplicam a todos os pools deste grupo através da camada de cota compartilhada.",
|
||||
"groupNamePrompt": "Digite o nome do grupo",
|
||||
"wizardGroupLabel": "Grupo"
|
||||
"wizardGroupLabel": "Grupo",
|
||||
"allGroups": "Todos os grupos"
|
||||
},
|
||||
"requestLogger": {
|
||||
"recording": "Recording",
|
||||
|
||||
@@ -130,10 +130,12 @@ test("QuotaSharePageClient: filters pool list by selectedGroupId (filteredPools)
|
||||
});
|
||||
|
||||
test("QuotaSharePageClient: renders group heading above pool grid", () => {
|
||||
// The group heading shows the group name and pool count
|
||||
// The group heading shows the group name and pool count.
|
||||
// Task 4 replaced the single-group heading (filteredPools.length) with
|
||||
// per-group headings using groupPools.length — one section per group.
|
||||
assert.ok(
|
||||
pageSrc.includes("filteredPools.length"),
|
||||
"QuotaSharePageClient must show filteredPools count in the group heading"
|
||||
pageSrc.includes("groupPools.length") || pageSrc.includes("filteredPools.length"),
|
||||
"QuotaSharePageClient must show pool count in the group heading (groupPools.length or filteredPools.length)"
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
152
tests/unit/quota-share-layout-v2.test.ts
Normal file
152
tests/unit/quota-share-layout-v2.test.ts
Normal file
@@ -0,0 +1,152 @@
|
||||
/**
|
||||
* tests/unit/quota-share-layout-v2.test.ts
|
||||
*
|
||||
* Task 4 — source-level assertions for the "all groups" default,
|
||||
* stacked group sections, and 3-column card grid in QuotaSharePageClient.
|
||||
*
|
||||
* Pattern mirrors tests/unit/quota-groups-ui.test.ts (source-scan).
|
||||
* Node.js native test runner — no DOM setup required.
|
||||
*/
|
||||
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { readFileSync } from "node:fs";
|
||||
import { join, dirname } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const ROOT = join(dirname(fileURLToPath(import.meta.url)), "..", "..");
|
||||
|
||||
const PAGE_CLIENT_PATH = join(
|
||||
ROOT,
|
||||
"src/app/(dashboard)/dashboard/costs/quota-share/QuotaSharePageClient.tsx"
|
||||
);
|
||||
|
||||
const EN_PATH = join(ROOT, "src/i18n/messages/en.json");
|
||||
const PT_PATH = join(ROOT, "src/i18n/messages/pt-BR.json");
|
||||
|
||||
const pageSrc = readFileSync(PAGE_CLIENT_PATH, "utf8");
|
||||
|
||||
// ── 1. Default state is "all" ────────────────────────────────────────────────
|
||||
|
||||
test('QuotaSharePageClient: selectedGroupId defaults to "all"', () => {
|
||||
assert.ok(
|
||||
pageSrc.includes('useState<string>("all")'),
|
||||
'QuotaSharePageClient must default selectedGroupId to "all" via useState<string>("all")'
|
||||
);
|
||||
});
|
||||
|
||||
// ── 2. "All groups" option in <select> ──────────────────────────────────────
|
||||
|
||||
test('QuotaSharePageClient: <select> has an option with value="all" using t("allGroups")', () => {
|
||||
assert.ok(
|
||||
pageSrc.includes('value="all"'),
|
||||
'QuotaSharePageClient <select> must include an <option value="all">'
|
||||
);
|
||||
assert.ok(
|
||||
pageSrc.includes('t("allGroups")') || pageSrc.includes("t('allGroups')"),
|
||||
'The "all" option must use the t("allGroups") i18n key'
|
||||
);
|
||||
});
|
||||
|
||||
// ── 3. Multi-group render (groupsToRender iteration) ─────────────────────────
|
||||
|
||||
test("QuotaSharePageClient: renders groups by iterating groupsToRender (all-groups mode)", () => {
|
||||
// Must derive groupsToRender (or equivalent) and map over it
|
||||
assert.ok(
|
||||
pageSrc.includes("groupsToRender"),
|
||||
"QuotaSharePageClient must define groupsToRender to iterate over groups"
|
||||
);
|
||||
// The render must map over groupsToRender to produce per-group sections
|
||||
assert.ok(
|
||||
pageSrc.includes("groupsToRender.map") || pageSrc.includes("groupsToRender.filter") || pageSrc.includes(".map((g)") || pageSrc.includes(".map((g, "),
|
||||
"QuotaSharePageClient must map over groupsToRender to render one section per group"
|
||||
);
|
||||
});
|
||||
|
||||
test("QuotaSharePageClient: computes groupsToRender from selectedGroupId === \"all\" check", () => {
|
||||
// The all-groups path: selectedGroupId === "all" ? groups : groups.filter(...)
|
||||
assert.ok(
|
||||
pageSrc.includes('selectedGroupId === "all"'),
|
||||
'QuotaSharePageClient must branch on selectedGroupId === "all" when computing groupsToRender'
|
||||
);
|
||||
});
|
||||
|
||||
// ── 4. 3-column grid ──────────────────────────────────────────────────────────
|
||||
|
||||
test("QuotaSharePageClient: card grid uses xl:grid-cols-3", () => {
|
||||
assert.ok(
|
||||
pageSrc.includes("xl:grid-cols-3"),
|
||||
"QuotaSharePageClient card grid className must include xl:grid-cols-3"
|
||||
);
|
||||
});
|
||||
|
||||
test("QuotaSharePageClient: card grid uses md:grid-cols-2", () => {
|
||||
assert.ok(
|
||||
pageSrc.includes("md:grid-cols-2"),
|
||||
"QuotaSharePageClient card grid className must include md:grid-cols-2 (intermediate breakpoint)"
|
||||
);
|
||||
});
|
||||
|
||||
// ── 5. Rename button hidden/disabled when "all" is selected ──────────────────
|
||||
|
||||
test('QuotaSharePageClient: rename button is hidden/disabled when selectedGroupId === "all"', () => {
|
||||
// When selectedGroupId === "all", the rename button should be hidden or disabled.
|
||||
// The old guard was `selectedGroupId !== "group-demo"`.
|
||||
// New guard must exclude "all" (either explicit !== "all" or a more general check).
|
||||
assert.ok(
|
||||
pageSrc.includes('selectedGroupId !== "all"') ||
|
||||
// guard via a truthy group lookup (groups.find(g.id === selectedGroupId) is falsy for "all")
|
||||
pageSrc.includes("groups.find((g) => g.id === selectedGroupId)") ||
|
||||
pageSrc.includes("groups.find(g => g.id === selectedGroupId)"),
|
||||
'Rename button must be hidden when selectedGroupId === "all" (guard via !== "all" or group lookup)'
|
||||
);
|
||||
});
|
||||
|
||||
// ── 6. i18n parity: allGroups key ────────────────────────────────────────────
|
||||
|
||||
test('i18n en.json: quotaShare.allGroups exists and equals "All groups"', () => {
|
||||
const en = JSON.parse(readFileSync(EN_PATH, "utf8")) as Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
>;
|
||||
assert.equal(
|
||||
typeof en["quotaShare"]?.["allGroups"],
|
||||
"string",
|
||||
"en.json must have quotaShare.allGroups"
|
||||
);
|
||||
assert.equal(
|
||||
en["quotaShare"]["allGroups"],
|
||||
"All groups",
|
||||
'en.json quotaShare.allGroups must equal "All groups"'
|
||||
);
|
||||
});
|
||||
|
||||
test('i18n pt-BR.json: quotaShare.allGroups exists and equals "Todos os grupos"', () => {
|
||||
const pt = JSON.parse(readFileSync(PT_PATH, "utf8")) as Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
>;
|
||||
assert.equal(
|
||||
typeof pt["quotaShare"]?.["allGroups"],
|
||||
"string",
|
||||
"pt-BR.json must have quotaShare.allGroups"
|
||||
);
|
||||
assert.equal(
|
||||
pt["quotaShare"]["allGroups"],
|
||||
"Todos os grupos",
|
||||
'pt-BR.json quotaShare.allGroups must equal "Todos os grupos"'
|
||||
);
|
||||
});
|
||||
|
||||
test("i18n parity: allGroups present in both en and pt-BR", () => {
|
||||
const en = JSON.parse(readFileSync(EN_PATH, "utf8")) as Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
>;
|
||||
const pt = JSON.parse(readFileSync(PT_PATH, "utf8")) as Record<
|
||||
string,
|
||||
Record<string, string>
|
||||
>;
|
||||
assert.ok("allGroups" in (en["quotaShare"] ?? {}), "en.json missing quotaShare.allGroups");
|
||||
assert.ok("allGroups" in (pt["quotaShare"] ?? {}), "pt-BR.json missing quotaShare.allGroups");
|
||||
});
|
||||
Reference in New Issue
Block a user