Add deleting suggestion of themselves

This commit is contained in:
Kalle
2022-06-24 14:55:59 +03:00
parent 5544a489a0
commit 640a2bf890
8 changed files with 132 additions and 36 deletions

View File

@@ -1,5 +1,6 @@
import { formatDistance } from "date-fns";
import type { MonthYear } from "~/modules/plus-server";
import { nextNonCompletedVoting } from "~/modules/plus-server";
import { atOrError } from "~/utils/arrays";
import { databaseTimestampToDate } from "~/utils/dates";
import { sql } from "../sql";
@@ -191,3 +192,16 @@ const delStm = sql.prepare(`
export function del(id: PlusSuggestion["id"]) {
delStm.run({ id });
}
const deleteAllStm = sql.prepare(`
DELETE FROM "PlusSuggestion"
WHERE
"suggestedId" = $suggestedId
AND tier = $tier
AND month = $month
AND year = $year
`);
export function deleteAll(args: Pick<PlusSuggestion, "suggestedId" | "tier">) {
deleteAllStm.run({ ...args, ...nextNonCompletedVoting(new Date()) });
}

View File

@@ -86,13 +86,24 @@ function fakeUser() {
discordAvatar: null,
discordDiscriminator: String(faker.random.numeric(4)),
discordId: String(faker.random.numeric(17)),
discordName: faker.random.word(),
discordName: uniqueDiscordName(),
twitch: null,
twitter: null,
youtubeId: null,
};
}
const usedNames = new Set<string>();
function uniqueDiscordName() {
let result = faker.random.word();
while (usedNames.has(result)) {
result = faker.random.word();
}
usedNames.add(result);
return result;
}
const idToPlusTier = (id: number) => {
if (id < 30) return 1;
if (id < 80) return 2;

View File

@@ -130,6 +130,10 @@ function suggestionHasNoOtherComments({
throw new Error(`Invalid suggestion id: ${suggestionId}`);
}
export function canDeleteSuggestionOfThemselves() {
return !isVotingActive();
}
interface CanSuggestNewUserFEArgs {
user?: Pick<UserWithPlusTier, "id" | "plusTier">;
suggestions: plusSuggestions.FindVisibleForUser;

View File

@@ -23,6 +23,7 @@ import {
canAddCommentToSuggestionFE,
canSuggestNewUserFE,
canDeleteComment,
canDeleteSuggestionOfThemselves,
} from "~/permissions";
import { makeTitle, parseRequestFormData, validate } from "~/utils/remix";
import { discordFullName } from "~/utils/strings";
@@ -30,6 +31,8 @@ import { actualNumber } from "~/utils/zod";
import { userPage } from "~/utils/urls";
import { RelativeTime } from "~/components/RelativeTime";
import { databaseTimestampToDate } from "~/utils/dates";
import { PLUS_TIERS } from "~/constants";
import { assertUnreachable } from "~/utils/types";
export const meta: MetaFunction = () => {
return {
@@ -38,9 +41,22 @@ export const meta: MetaFunction = () => {
};
};
const suggestionActionSchema = z.object({
suggestionId: z.preprocess(actualNumber, z.number()),
});
const suggestionActionSchema = z.union([
z.object({
_action: z.literal("DELETE_COMMENT"),
suggestionId: z.preprocess(actualNumber, z.number()),
}),
z.object({
_action: z.literal("DELETE_SUGGESTION_OF_THEMSELVES"),
tier: z.preprocess(
actualNumber,
z
.number()
.min(Math.min(...PLUS_TIERS))
.max(Math.max(...PLUS_TIERS))
),
}),
]);
export const action: ActionFunction = async ({ request }) => {
const data = await parseRequestFormData({
@@ -49,30 +65,46 @@ export const action: ActionFunction = async ({ request }) => {
});
const user = await requireUser(request);
const suggestions = db.plusSuggestions.findVisibleForUser({
...nextNonCompletedVoting(new Date()),
plusTier: user.plusTier,
});
switch (data._action) {
case "DELETE_COMMENT": {
const suggestions = db.plusSuggestions.findVisibleForUser({
...nextNonCompletedVoting(new Date()),
plusTier: user.plusTier,
});
const targetSuggestion = suggestions
? Object.values(suggestions)
?.flat()
.flatMap((u) => u.suggestions)
.find((s) => s.id === data.suggestionId)
: undefined;
const targetSuggestion = suggestions
? Object.values(suggestions)
?.flat()
.flatMap((u) => u.suggestions)
.find((s) => s.id === data.suggestionId)
: undefined;
validate(suggestions);
validate(targetSuggestion);
validate(
canDeleteComment({
user,
author: targetSuggestion.author,
suggestionId: data.suggestionId,
suggestions,
})
);
validate(suggestions);
validate(targetSuggestion);
validate(
canDeleteComment({
user,
author: targetSuggestion.author,
suggestionId: data.suggestionId,
suggestions,
})
);
db.plusSuggestions.del(data.suggestionId);
db.plusSuggestions.del(data.suggestionId);
break;
}
case "DELETE_SUGGESTION_OF_THEMSELVES": {
validate(canDeleteSuggestionOfThemselves());
db.plusSuggestions.deleteAll({ suggestedId: user.id, tier: data.tier });
break;
}
default: {
assertUnreachable(data);
}
}
return null;
};
@@ -229,10 +261,30 @@ function SuggestedForInfo() {
}
return (
<div className="plus__suggested-info-text">
You are suggested for{" "}
{data.suggestedForTiers.map((tier) => `+${tier}`).join(" and ")} this
month.
<div className="stack md">
<div className="plus__suggested-info-text">
You are suggested to{" "}
{data.suggestedForTiers.map((tier) => `+${tier}`).join(" and ")} this
month.
</div>
{canDeleteSuggestionOfThemselves() ? (
<div className="stack vertical md">
{data.suggestedForTiers.map((tier) => (
<FormWithConfirm
key={tier}
fields={[
["_action", "DELETE_SUGGESTION_OF_THEMSELVES"],
["tier", tier],
]}
dialogHeading={`Delete your suggestion to +${tier}? You won't appear in next voting.`}
>
<Button key={tier} tiny variant="destructive" type="submit">
Delete your +{tier} suggestion
</Button>
</FormWithConfirm>
))}
</div>
) : null}
</div>
);
}
@@ -375,7 +427,10 @@ function CommentDeleteButton({
}) {
return (
<FormWithConfirm
fields={[["suggestionId", suggestionId]]}
fields={[
["suggestionId", suggestionId],
["_action", "DELETE_COMMENT"],
]}
dialogHeading={
isFirstSuggestion
? `Delete your suggestion of ${suggestedDiscordName} to +${tier}?`

View File

@@ -5,7 +5,7 @@ import { z } from "zod";
import { Button, LinkButton } from "~/components/Button";
import { Dialog } from "~/components/Dialog";
import { Redirect } from "~/components/Redirect";
import { PlUS_SUGGESTION_COMMENT_MAX_LENGTH } from "~/constants";
import { PlUS_SUGGESTION_COMMENT_MAX_LENGTH, PLUS_TIERS } from "~/constants";
import { nextNonCompletedVoting } from "~/modules/plus-server";
import { db } from "~/db";
import { requireUser, useUser } from "~/modules/auth";
@@ -22,7 +22,13 @@ import { CommentTextarea } from "./new";
const commentActionSchema = z.object({
text: z.string().min(1).max(PlUS_SUGGESTION_COMMENT_MAX_LENGTH),
tier: z.preprocess(actualNumber, z.number().min(1).max(3)),
tier: z.preprocess(
actualNumber,
z
.number()
.min(Math.min(...PLUS_TIERS))
.max(Math.max(...PLUS_TIERS))
),
suggestedId: z.preprocess(actualNumber, z.number()),
});

View File

@@ -34,7 +34,13 @@ import { atOrError } from "~/utils/arrays";
import { requireUser, useUser } from "~/modules/auth";
const commentActionSchema = z.object({
tier: z.preprocess(actualNumber, z.number().min(1).max(3)),
tier: z.preprocess(
actualNumber,
z
.number()
.min(Math.min(...PLUS_TIERS))
.max(Math.max(...PLUS_TIERS))
),
text: z.string().min(1).max(PlUS_SUGGESTION_FIRST_COMMENT_MAX_LENGTH),
"user[value]": z.preprocess(actualNumber, z.number().positive()),
});

View File

@@ -3,8 +3,7 @@ import { defineConfig } from "cypress";
export default defineConfig({
fixturesFolder: false,
e2e: {
// eslint-disable-next-line @typescript-eslint/no-empty-function
setupNodeEvents(on, config) {},
// setupNodeEvents(on, config) {},
baseUrl: "http://localhost:4455",
},
});

View File

@@ -77,5 +77,6 @@ describe("Plus voting results page", () => {
cy.contains("Sendou");
});
// xxx: describe Plus Voting
// xxx: figure out a good way to do plus server tests with serverside isVotingActive or delete existing tests
// -> if way is found then should also add tests for voting
});