Add match id (optional) to user report

This commit is contained in:
Kalle
2026-07-19 14:59:57 +03:00
parent bce7523ed1
commit 3c3508dd7e
41 changed files with 180 additions and 9 deletions

View File

@@ -3177,6 +3177,12 @@ async function userReports() {
0, 0, 0, 1, 2, 2, 2, 2, 5, 5, 7, 8, 10, 11, 11,
];
// attach a real SendouQ match to some reports so the admin tab's match links have data
const someMatch = await db
.selectFrom("GroupMatch")
.select("id")
.executeTakeFirst();
await db
.insertInto("UserReport")
.values(
@@ -3185,6 +3191,7 @@ async function userReports() {
reporterUserId: 10 + i,
category: USER_REPORT_CATEGORIES[i % USER_REPORT_CATEGORIES.length],
description: faker.lorem.sentences({ min: 1, max: 3 }),
matchId: i % 3 === 0 ? (someMatch?.id ?? null) : null,
createdAt: dateToDatabaseTimestamp(
sub(new Date(), { months: monthsAgo, days: (i * 3) % 7, hours: i }),
),

View File

@@ -1305,6 +1305,7 @@ export interface UserReport {
reporterUserId: number;
category: UserReportCategory;
description: string;
matchId: number | null;
createdAt: Generated<number>;
}

View File

@@ -33,6 +33,17 @@ import * as PlayerStatRepository from "./PlayerStatRepository.server";
import * as ReportedWeaponRepository from "./ReportedWeaponRepository.server";
import * as SkillRepository from "./SkillRepository.server";
/** Whether a GroupMatch with the given id exists. */
export async function exists(id: number) {
const row = await db
.selectFrom("GroupMatch")
.select("id")
.where("id", "=", id)
.executeTakeFirst();
return Boolean(row);
}
export async function findById(id: number) {
const result = await db
.selectFrom("GroupMatch")

View File

@@ -12,7 +12,7 @@ import { USER_REPORT_CATEGORY_LABELS } from "~/features/user-report/user-report-
import { SendouForm } from "~/form";
import { useFormatDistanceToNow } from "~/hooks/intl/useFormatDistanceToNow";
import invariant from "~/utils/invariant";
import { userPage } from "~/utils/urls";
import { sendouQMatchPage, userPage } from "~/utils/urls";
import { action } from "../actions/u.$identifier.admin.server";
import { SubPageHeader } from "../components/SubPageHeader";
import { loader } from "../loaders/u.$identifier.admin.server";
@@ -230,6 +230,14 @@ function Reports() {
- {formatDistanceToNow(report.createdAt)}
</summary>
<p className="ml-2 whitespace-pre-wrap">{report.description}</p>
{report.matchId !== null ? (
<Link
className="ml-2 text-xs"
to={sendouQMatchPage(report.matchId)}
>
SendouQ match #{report.matchId}
</Link>
) : null}
</details>
))}
</div>

View File

@@ -26,6 +26,7 @@ export function upsert(
oc.columns(["reportedUserId", "reporterUserId"]).doUpdateSet({
category: args.category,
description: args.description,
matchId: args.matchId,
createdAt: databaseTimestampNow(),
}),
)
@@ -65,6 +66,7 @@ export function findAllByReportedUserId(reportedUserId: number) {
"UserReport.id",
"UserReport.category",
"UserReport.description",
"UserReport.matchId",
"UserReport.createdAt",
"UserReport.reporterUserId",
"User.username as reporterUsername",

View File

@@ -1,4 +1,5 @@
import { useTranslation } from "react-i18next";
import { useMatches } from "react-router";
import { SendouDialog } from "~/components/elements/Dialog";
import { toastQueue } from "~/components/elements/Toast";
import { FormMessage } from "~/components/FormMessage";
@@ -6,6 +7,8 @@ import { SendouForm } from "~/form";
import { userReportPage } from "~/utils/urls";
import { reportUserSchema } from "../user-report-schemas";
const SENDOUQ_MATCH_ROUTE_ID = "features/sendouq-match/routes/q.match.$id";
/**
* Modal for reporting a user to the staff, posting to the `/user-report/:id` resource
* route. Re-reporting the same user overwrites the previous report. Rendered wherever
@@ -21,6 +24,7 @@ export function ReportUserDialog({
onClose: () => void;
}) {
const { t } = useTranslation(["user"]);
const prefilledMatchId = useSendouQMatchIdFromRoute();
return (
<SendouDialog
@@ -30,6 +34,7 @@ export function ReportUserDialog({
<SendouForm
schema={reportUserSchema}
action={userReportPage(userId)}
defaultValues={{ matchId: prefilledMatchId }}
onSuccess={() => {
toastQueue.add(
{ message: "Report sent to the staff", variant: "success" },
@@ -42,6 +47,7 @@ export function ReportUserDialog({
<>
<FormField name="category" />
<FormField name="description" />
<FormField name="matchId" />
<FormMessage type="info">
{t("user:card.report.falseReportsWarning")}
</FormMessage>
@@ -51,3 +57,17 @@ export function ReportUserDialog({
</SendouDialog>
);
}
/**
* Reads the SendouQ match id from the current route so a report opened from a match page
* prefills its "Match ID" field. Returns `undefined` on any other page.
*/
function useSendouQMatchIdFromRoute() {
const matches = useMatches();
const matchRoute = matches.find(
(match) => match.id === SENDOUQ_MATCH_ROUTE_ID,
);
return matchRoute?.params.id;
}

View File

@@ -1,6 +1,6 @@
import type { UserReportCategory } from "~/db/tables";
import { logger } from "~/utils/logger";
import { SENDOU_INK_BASE_URL, userPage } from "~/utils/urls";
import { SENDOU_INK_BASE_URL, sendouQMatchPage, userPage } from "~/utils/urls";
import { USER_REPORT_CATEGORY_LABELS } from "../user-report-constants";
const EMBED_DESCRIPTION_MAX_LENGTH = 1000;
@@ -15,6 +15,7 @@ export function sendUserReportWebhook(args: {
reporter: { username: string; discordId: string; customUrl: string | null };
category: UserReportCategory;
description: string;
matchId: number | null;
isUpdate: boolean;
reportCounts: { lastMonth: number; lastYear: number };
}) {
@@ -50,6 +51,14 @@ export function sendUserReportWebhook(args: {
name: "Description",
value: truncate(args.description),
},
...(args.matchId !== null
? [
{
name: "SendouQ match",
value: `[#${args.matchId}](${SENDOU_INK_BASE_URL}${sendouQMatchPage(args.matchId)})`,
},
]
: []),
{
name: "Reports against this user",
value: `Last month: ${args.reportCounts.lastMonth} • Last year: ${args.reportCounts.lastYear}`,

View File

@@ -9,10 +9,8 @@ import {
} from "~/utils/remix.server";
import { sendUserReportWebhook } from "../core/discord-webhook.server";
import * as UserReportRepository from "../UserReportRepository.server";
import {
reportUserParamsSchema,
reportUserSchema,
} from "../user-report-schemas";
import { reportUserParamsSchema } from "../user-report-schemas";
import { reportUserSchemaServer } from "../user-report-schemas.server";
export const action = async ({ request, params }: ActionFunctionArgs) => {
const user = requireUser();
@@ -28,7 +26,10 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
await UserRepository.findLeanById(reportedUserId),
);
const result = await parseFormData({ request, schema: reportUserSchema });
const result = await parseFormData({
request,
schema: reportUserSchemaServer,
});
if (!result.success) {
return { fieldErrors: result.fieldErrors };
}
@@ -38,6 +39,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
reporterUserId: user.id,
category: result.data.category,
description: result.data.description,
matchId: result.data.matchId,
});
const reportCounts =
@@ -48,6 +50,7 @@ export const action = async ({ request, params }: ActionFunctionArgs) => {
reporter: user,
category: result.data.category,
description: result.data.description,
matchId: result.data.matchId,
isUpdate,
reportCounts,
});

View File

@@ -2,6 +2,7 @@ import type { UserReportCategory } from "~/db/tables";
export const USER_REPORT = {
DESCRIPTION_MAX_LENGTH: 2000,
MATCH_ID_MAX_LENGTH: 10,
};
/** English display names, shown on the staff-only admin tab and in the Discord webhook embed. */

View File

@@ -0,0 +1,22 @@
import { z } from "zod";
import * as SQMatchRepository from "~/features/sendouq-match/SQMatchRepository.server";
import { reportUserSchema } from "./user-report-schemas";
export const reportUserSchemaServer = z.object({
...reportUserSchema.shape,
// cast to the concrete value type: the field's `.nullable()` makes its inferred
// type a union that Zod's `.refine` overload can't resolve
matchId: (reportUserSchema.shape.matchId as z.ZodType<string | null>)
.refine(
async (matchId) => {
if (!matchId) return true;
const id = Number(matchId);
if (!Number.isInteger(id) || id <= 0) return false;
return SQMatchRepository.exists(id);
},
{ message: "forms:errors.matchNotFound" },
)
.transform((matchId) => (matchId ? Number(matchId) : null)),
});

View File

@@ -1,5 +1,5 @@
import { z } from "zod";
import { select, textAreaRequired } from "~/form/fields";
import { select, textAreaRequired, textFieldOptional } from "~/form/fields";
import { id } from "~/utils/zod";
import { USER_REPORT } from "./user-report-constants";
@@ -21,6 +21,11 @@ export const reportUserSchema = z.object({
label: "labels.description",
maxLength: USER_REPORT.DESCRIPTION_MAX_LENGTH,
}),
matchId: textFieldOptional({
label: "labels.reportMatchId",
bottomText: "bottomTexts.reportMatchId",
maxLength: USER_REPORT.MATCH_ID_MAX_LENGTH,
}),
});
export const reportUserParamsSchema = z.object({

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -1,5 +1,6 @@
import { NZAP_TEST_ID } from "~/db/seed/constants";
import { LFG_PAGE } from "~/utils/urls";
import { ADMIN_ID } from "~/features/admin/admin-constants";
import { LFG_PAGE, SENDOUQ_PAGE, sendouQMatchPage } from "~/utils/urls";
import {
expect,
impersonate,
@@ -41,4 +42,24 @@ test.describe("User report", () => {
await reportsList.locator("summary").first().click();
await expect(page.getByText(description)).toBeVisible();
});
test("prefills the match id when reporting from a match page", async ({
page,
}) => {
await seed(page, "IN_SQ_MATCH");
await impersonate(page, ADMIN_ID);
await navigate({ page, url: SENDOUQ_PAGE });
await expect(page).toHaveURL(/\/q\/match\/\d+/);
const matchId = page.url().split("/match/")[1];
await navigate({
page,
url: `${sendouQMatchPage(Number(matchId))}?tab=rosters`,
});
await page.getByRole("button", { name: /N-ZAP/ }).first().click();
await page.getByTestId("report-user-button").click();
await expect(page.getByLabel("Match ID")).toHaveValue(matchId);
});
});

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "Tentatek",
"options.xpDivision.JPN": "Takoroka",
"bottomTexts.unverifiedXp": "Self-report a peak XP higher than your verified X Rank XP, by up to 200 points. Requires linked X Rank placements to report against.",
"bottomTexts.reportMatchId": "SendouQ match this report relates to (optional).",
"errors.unverifiedXpNotAboveVerified": "Peak XP must be higher than your verified X Rank XP, by at most 200 points. Link your X Rank placements first.",
"labels.link": "Link",
"labels.tag": "Tag",
@@ -59,6 +60,7 @@
"errors.minLength": "Must be at least {{min}} characters",
"errors.maxLength": "Must be at most {{max}} characters",
"errors.mustBeWholeNumber": "Must be a whole number (no decimals)",
"errors.matchNotFound": "No SendouQ match found with this ID",
"errors.invalidUrl": "Must be a valid URL",
"errors.notAllowedCharacters": "Contains not allowed characters",
"errors.atLeastOneOption": "At least one option must be selected",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "Ban expiration date",
"bottomTexts.banUserExpiresAtHelp": "Leave empty for a permanent ban",
"labels.reportCategory": "Category",
"labels.reportMatchId": "Match ID",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "Inappropriate content",
"options.userReportCategory.ALTING": "Alting",
"options.userReportCategory.HARASSMENT": "Harassment",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "Etiqueta",
@@ -59,6 +60,7 @@
"errors.minLength": "Debe tener al menos {{min}} caracteres",
"errors.maxLength": "Debe tener como máximo {{max}} caracteres",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "Debe ser una URL válida",
"errors.notAllowedCharacters": "Contiene caracteres no permitidos",
"errors.atLeastOneOption": "Debe seleccionarse al menos una opción",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "Fecha de expiración del baneo",
"bottomTexts.banUserExpiresAtHelp": "Deja vacío para un baneo permanente",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "",
"labels.tag": "",
@@ -59,6 +60,7 @@
"errors.minLength": "",
"errors.maxLength": "",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "",
"errors.notAllowedCharacters": "",
"errors.atLeastOneOption": "",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "",
"bottomTexts.banUserExpiresAtHelp": "",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -16,6 +16,7 @@
"options.xpDivision.WEST": "",
"options.xpDivision.JPN": "",
"bottomTexts.unverifiedXp": "",
"bottomTexts.reportMatchId": "",
"errors.unverifiedXpNotAboveVerified": "",
"labels.link": "链接",
"labels.tag": "标签",
@@ -59,6 +60,7 @@
"errors.minLength": "字数不能少于 {{min}} 个字符",
"errors.maxLength": "字数不能多于 {{max}} 个字符",
"errors.mustBeWholeNumber": "",
"errors.matchNotFound": "",
"errors.invalidUrl": "请输入有效的 URL 地址",
"errors.notAllowedCharacters": "包含非法字符",
"errors.atLeastOneOption": "请至少选择一个选项",
@@ -224,6 +226,7 @@
"labels.banUserExpiresAt": "封禁结束日期",
"bottomTexts.banUserExpiresAtHelp": "留空则表示永久封禁",
"labels.reportCategory": "",
"labels.reportMatchId": "",
"options.userReportCategory.INAPPROPRIATE_CONTENT": "",
"options.userReportCategory.ALTING": "",
"options.userReportCategory.HARASSMENT": "",

View File

@@ -0,0 +1,13 @@
export function up(db) {
db.transaction(() => {
db.prepare(
/* sql */ `
alter table "UserReport"
add column "matchId" integer
references "GroupMatch"("id") on delete set null
`,
).run();
db.pragma("foreign_key_check");
})();
}