Add options to delete team images (#2328)

* Add options to delete team images

* Update test

* Add image deleting to test
This commit is contained in:
hfcRed 2025-05-26 17:58:29 +02:00 committed by GitHub
parent 5cc0be347f
commit 5c78b60b2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 206 additions and 12 deletions

View File

@ -259,6 +259,37 @@ export function del(teamId: number) {
});
}
export function removeTeamImage(
teamId: number,
imageType: "avatar" | "banner",
) {
const imageIdField = imageType === "avatar" ? "avatarImgId" : "bannerImgId";
return db.transaction().execute(async (trx) => {
const team = await trx
.selectFrom("Team")
.select(imageIdField)
.where("id", "=", teamId)
.executeTakeFirst();
const imageId = team?.[imageIdField];
if (imageId) {
await trx
.deleteFrom("UnvalidatedUserSubmittedImage")
.where("id", "=", imageId)
.execute();
}
await trx
.updateTable("AllTeam")
.set({
[imageIdField]: null,
})
.where("id", "=", teamId)
.execute();
});
}
export function resetInviteCode(teamId: number) {
return db
.updateTable("AllTeam")

View File

@ -28,17 +28,26 @@ export const action: ActionFunction = async ({ request, params }) => {
schema: editTeamSchema,
});
if (data._action.includes("DELETE")) {
errorToastIfFalsy(
isTeamOwner({ team, user }),
"You are not the team owner",
);
}
switch (data._action) {
case "DELETE": {
errorToastIfFalsy(
isTeamOwner({ team, user }),
"You are not the team owner",
);
case "DELETE_TEAM": {
await TeamRepository.del(team.id);
throw redirect(TEAM_SEARCH_PAGE);
}
case "DELETE_AVATAR": {
await TeamRepository.removeTeamImage(team.id, "avatar");
throw redirect(teamPage(team.customUrl));
}
case "DELETE_BANNER": {
await TeamRepository.removeTeamImage(team.id, "banner");
throw redirect(teamPage(team.customUrl));
}
case "EDIT": {
const newCustomUrl = mySlugify(data.name);
const existingTeam = await TeamRepository.findByCustomUrl(newCustomUrl);

View File

@ -67,7 +67,7 @@ export default function EditTeamPage() {
{isTeamOwner({ team, user }) ? (
<FormWithConfirm
dialogHeading={t("team:deleteTeam.header", { teamName: team.name })}
fields={[["_action", "DELETE"]]}
fields={[["_action", "DELETE_TEAM"]]}
>
<Button
className="ml-auto"
@ -80,6 +80,7 @@ export default function EditTeamPage() {
) : null}
<Form method="post" className="stack md items-start">
<ImageUploadLinks />
<ImageRemoveButtons />
{canAddCustomizedColors(team) ? (
<CustomizedColorsInput initialColors={css} />
) : null}
@ -132,6 +133,57 @@ function ImageUploadLinks() {
);
}
function ImageRemoveButtons() {
const { t } = useTranslation(["common", "team"]);
const { team } = useLoaderData<typeof loader>();
return team.avatarSrc || team.bannerSrc ? (
<div>
<Label>{t("team:forms.fields.removeImages")}</Label>
<ol className="team__image-links-list">
{team.avatarSrc ? (
<li>
<FormWithConfirm
dialogHeading={t("team:deleteTeam.profilePicture.header", {
teamName: team.name,
})}
fields={[["_action", "DELETE_AVATAR"]]}
submitButtonText={t("common:actions.remove")}
>
<Button
className="ml-auto"
variant="minimal-destructive"
data-testid="delete-team-button"
>
{t("team:actionButtons.deleteTeam.profilePicture")}
</Button>
</FormWithConfirm>
</li>
) : null}
{team.bannerSrc ? (
<li>
<FormWithConfirm
dialogHeading={t("team:deleteTeam.banner.header", {
teamName: team.name,
})}
fields={[["_action", "DELETE_BANNER"]]}
submitButtonText={t("common:actions.remove")}
>
<Button
className="ml-auto"
variant="minimal-destructive"
data-testid="delete-team-button"
>
{t("team:actionButtons.deleteTeam.banner")}
</Button>
</FormWithConfirm>
</li>
) : null}
</ol>
</div>
) : null;
}
function NameInput() {
const { t } = useTranslation(["common", "team"]);
const { team } = useLoaderData<typeof loader>();

View File

@ -104,7 +104,7 @@ describe("Secondary teams", () => {
await editTeamAction(
{
_action: "DELETE",
_action: "DELETE_TEAM",
},
{
user: "regular",
@ -185,4 +185,95 @@ describe("Secondary teams", () => {
const { secondaryTeams } = await loadTeams();
expect(secondaryTeams).toHaveLength(2);
});
const createTeamWithImage = async (imageType: "avatar" | "banner") => {
await createTeamAction({ name: "Team 1" }, { user: "regular" });
const imageId = await db
.insertInto("UnvalidatedUserSubmittedImage")
.values({
url: `https://example.com/test-${imageType}.jpg`,
submitterUserId: REGULAR_USER_TEST_ID,
})
.returning("id")
.executeTakeFirstOrThrow();
const imageField = imageType === "avatar" ? "avatarImgId" : "bannerImgId";
await db
.updateTable("AllTeam")
.set({ [imageField]: imageId.id })
.where("customUrl", "=", "team-1")
.execute();
return imageId.id;
};
it("deletes team avatar", async () => {
const imageId = await createTeamWithImage("avatar");
await editTeamAction(
{ _action: "DELETE_AVATAR" },
{ user: "regular", params: { customUrl: "team-1" } },
);
const team = await db
.selectFrom("Team")
.select("avatarImgId")
.where("customUrl", "=", "team-1")
.executeTakeFirst();
expect(team?.avatarImgId).toBeNull();
const image = await db
.selectFrom("UnvalidatedUserSubmittedImage")
.select("id")
.where("id", "=", imageId)
.executeTakeFirst();
expect(image).toBeUndefined();
});
it("deletes team banner", async () => {
const imageId = await createTeamWithImage("banner");
await editTeamAction(
{ _action: "DELETE_BANNER" },
{ user: "regular", params: { customUrl: "team-1" } },
);
const team = await db
.selectFrom("Team")
.select("bannerImgId")
.where("customUrl", "=", "team-1")
.executeTakeFirst();
expect(team?.bannerImgId).toBeNull();
const image = await db
.selectFrom("UnvalidatedUserSubmittedImage")
.select("id")
.where("id", "=", imageId)
.executeTakeFirst();
expect(image).toBeUndefined();
});
it("only team owner can delete images", async () => {
await createTeamWithImage("avatar");
await db
.insertInto("User")
.values({
discordName: "otheruser",
discordId: "999",
})
.execute();
const response = await editTeamAction(
{ _action: "DELETE_AVATAR" },
{ user: "regular", params: { customUrl: "team-1" } },
);
expect(response.status).toBe(302);
});
});

View File

@ -26,10 +26,16 @@ export const teamProfilePageActionSchema = z.union([
}),
]);
const deleteActionsSchema = z.object({
_action: z.union([
_action("DELETE_TEAM"),
_action("DELETE_AVATAR"),
_action("DELETE_BANNER"),
]),
});
export const editTeamSchema = z.union([
z.object({
_action: _action("DELETE"),
}),
deleteActionsSchema,
z.object({
_action: _action("EDIT"),
name: z.string().min(TEAM.NAME_MIN_LENGTH).max(TEAM.NAME_MAX_LENGTH),

View File

@ -9,12 +9,16 @@
"actionButtons.editTeam": "Edit Team",
"actionButtons.manageRoster": "Manage Roster",
"actionButtons.deleteTeam": "Delete Team",
"actionButtons.deleteTeam.profilePicture": "Remove Profile Picture",
"actionButtons.deleteTeam.banner": "Remove Banner",
"actionButtons.kick": "Kick",
"kick.header": "Kick {{user}} from {{teamName}}?",
"actionButtons.transferOwnership": "Transfer Ownership",
"transferOwnership.header": "Transfer ownership of {{teamName}} to {{user}}?",
"actionButtons.transferOwnership.confirm": "Transfer",
"deleteTeam.header": "Are you sure you want to delete {{teamName}}?",
"deleteTeam.profilePicture.header": "Are you sure you want to remove the teams profile picture?",
"deleteTeam.banner.header": "Are you sure you want to remove the teams banner picture?",
"roles.CAPTAIN": "Captain",
"roles.CO_CAPTAIN": "Co-Captain",
"roles.FRONTLINE": "Frontline",
@ -30,6 +34,7 @@
"forms.fields.teamBsky": "Team Bluesky",
"forms.fields.bio": "Bio",
"forms.fields.uploadImages": "Upload images",
"forms.fields.removeImages": "Remove images",
"forms.fields.uploadImages.pfp": "Profile Picture",
"forms.fields.uploadImages.banner": "Team Picture Banner",
"forms.info.name": "Note that if you change your team's name then someone else can claim the name and URL for their team",