feat(team-calc) : add character-wide team import
This commit is contained in:
@@ -26,10 +26,10 @@ vi.mock("@/db", () => ({
|
||||
|
||||
import {
|
||||
artifactConditions, artifactProfiles, constellationConditions, extraSectionRows,
|
||||
guides, guideSections, teamConditions, weaponConditions,
|
||||
guides, guideSections, teamCompositions, teamConditions, teamMembers, weaponConditions,
|
||||
catalogMaterials, guidePosterBleeding, media,
|
||||
} from "@/db/schema";
|
||||
import { getAdminGuideContent } from "./queries";
|
||||
import { getAdminGuideContent, listAdminTeamImportsForCharacter } from "./queries";
|
||||
|
||||
beforeEach(() => {
|
||||
mocks.reads.length = 0;
|
||||
@@ -102,3 +102,32 @@ describe("admin section reads", () => {
|
||||
expect(mocks.reads.filter((table) => table === media)).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("team import lookup", () => {
|
||||
it("hydrates matching teams and source labels with one member read", async () => {
|
||||
mocks.rows.set(teamCompositions, [{
|
||||
id: "team-1", title: "Example", rotationSeconds: "20", combo: "E > Q",
|
||||
guideName: "Other Guide", conditionName: "Boss",
|
||||
}]);
|
||||
mocks.rows.set(teamMembers, [{
|
||||
member: {
|
||||
teamId: "team-1", characterKey: "character-1", weaponKey: "weapon-1",
|
||||
artifactKey: "artifact-1", secondArtifactKey: null,
|
||||
constellation: "2", refinement: "1", damage: "100",
|
||||
},
|
||||
characterName: "Character One",
|
||||
}]);
|
||||
|
||||
const result = await listAdminTeamImportsForCharacter("current-guide", "character-1");
|
||||
expect(result).toEqual([{
|
||||
id: "team-1", title: "Example", rotationSeconds: "20", combo: "E > Q",
|
||||
guideName: "Other Guide", conditionName: "Boss",
|
||||
members: [{
|
||||
characterKey: "character-1", characterName: "Character One", weaponKey: "weapon-1",
|
||||
artifactKey: "artifact-1", secondArtifactKey: null,
|
||||
constellation: "2", refinement: "1", damage: "100",
|
||||
}],
|
||||
}]);
|
||||
expect(mocks.reads).toEqual([teamCompositions, teamMembers]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -200,6 +200,64 @@ export async function listAdminTeamImportGuides(excludeGuideId: string) {
|
||||
}));
|
||||
}
|
||||
|
||||
export async function listAdminTeamImportsForCharacter(
|
||||
excludeGuideId: string,
|
||||
characterKey: string,
|
||||
) {
|
||||
const matches = await getDb()
|
||||
.select({
|
||||
id: teamCompositions.id,
|
||||
title: teamCompositions.title,
|
||||
rotationSeconds: teamCompositions.rotationSeconds,
|
||||
combo: teamCompositions.combo,
|
||||
guideName: guides.name,
|
||||
conditionName: teamConditions.name,
|
||||
})
|
||||
.from(teamCompositions)
|
||||
.innerJoin(guides, eq(guides.id, teamCompositions.guideId))
|
||||
.innerJoin(teamMembers, eq(teamMembers.teamId, teamCompositions.id))
|
||||
.leftJoin(teamConditions, eq(teamConditions.id, teamCompositions.conditionId))
|
||||
.where(and(
|
||||
eq(teamMembers.characterKey, characterKey),
|
||||
ne(guides.id, excludeGuideId),
|
||||
isNull(guides.trashedAt),
|
||||
))
|
||||
.orderBy(asc(guides.name), asc(guides.id), asc(teamConditions.sortOrder), asc(teamCompositions.sortOrder), asc(teamCompositions.id));
|
||||
|
||||
if (!matches.length) return [];
|
||||
const members = await getDb()
|
||||
.select({ member: teamMembers, characterName: catalogCharacters.name })
|
||||
.from(teamMembers)
|
||||
.leftJoin(catalogCharacters, eq(catalogCharacters.key, teamMembers.characterKey))
|
||||
.where(inArray(teamMembers.teamId, matches.map((team) => team.id)))
|
||||
.orderBy(asc(teamMembers.position));
|
||||
const membersByTeam = new Map<string, Array<{
|
||||
characterKey: string;
|
||||
characterName: string;
|
||||
weaponKey: string | null;
|
||||
artifactKey: string | null;
|
||||
secondArtifactKey: string | null;
|
||||
constellation: string;
|
||||
refinement: string;
|
||||
damage: string;
|
||||
}>>();
|
||||
for (const { member, characterName } of members) {
|
||||
const group = membersByTeam.get(member.teamId) ?? [];
|
||||
group.push({
|
||||
characterKey: member.characterKey,
|
||||
characterName: characterName ?? member.characterKey,
|
||||
weaponKey: member.weaponKey,
|
||||
artifactKey: member.artifactKey,
|
||||
secondArtifactKey: member.secondArtifactKey,
|
||||
constellation: member.constellation,
|
||||
refinement: member.refinement,
|
||||
damage: member.damage,
|
||||
});
|
||||
membersByTeam.set(member.teamId, group);
|
||||
}
|
||||
return matches.map((team) => ({ ...team, members: membersByTeam.get(team.id) ?? [] }));
|
||||
}
|
||||
|
||||
export async function getAdminTeamImportGuide(guideId: string) {
|
||||
const [guide] = await getDb()
|
||||
.select({ id: guides.id, name: guides.name })
|
||||
|
||||
@@ -3,6 +3,7 @@ import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
applyTeamFields,
|
||||
cloneTeamImports,
|
||||
filterTeamImportsByName,
|
||||
findInvalidTeamCatalogReference,
|
||||
findUnsupportedTeamWeapon,
|
||||
limitPresets,
|
||||
@@ -160,6 +161,12 @@ describe("team bulk tools", () => {
|
||||
expect(imported[0].members[0]).not.toBe(source.members[0]);
|
||||
});
|
||||
|
||||
it("ignores existing and repeated names unless duplicate filtering is disabled", () => {
|
||||
const candidates = [team("one", " Alpha "), team("two", "Beta"), team("three", "BETA")];
|
||||
expect(filterTeamImportsByName([team("current", "alpha")], candidates, true).map((item) => item.clientId)).toEqual(["two"]);
|
||||
expect(filterTeamImportsByName([team("current", "alpha")], candidates, false)).toEqual(candidates);
|
||||
});
|
||||
|
||||
it("rejects malformed and unsupported team transfers", () => {
|
||||
expect(() => parseTeamTransfer("not-json")).toThrow();
|
||||
expect(() => parseTeamTransfer(JSON.stringify({
|
||||
|
||||
@@ -125,6 +125,21 @@ export function cloneTeamImports(
|
||||
}));
|
||||
}
|
||||
|
||||
export function filterTeamImportsByName<T extends { title: string }>(
|
||||
currentTeams: readonly { title: string }[],
|
||||
candidates: readonly T[],
|
||||
ignoreSameName: boolean,
|
||||
): T[] {
|
||||
if (!ignoreSameName) return [...candidates];
|
||||
const names = new Set(currentTeams.map((team) => team.title.trim().toLocaleLowerCase()));
|
||||
return candidates.filter((team) => {
|
||||
const name = team.title.trim().toLocaleLowerCase();
|
||||
if (names.has(name)) return false;
|
||||
names.add(name);
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export function findInvalidTeamCatalogReference(
|
||||
team: Omit<TeamToolValue, "clientId">,
|
||||
characters: readonly TeamToolCatalogItem[],
|
||||
|
||||
Reference in New Issue
Block a user