diff --git a/app/db/seed/index.ts b/app/db/seed/index.ts index 05c72c2b7..2da620ddd 100644 --- a/app/db/seed/index.ts +++ b/app/db/seed/index.ts @@ -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 }), ), diff --git a/app/db/tables.ts b/app/db/tables.ts index a8ae7c21a..c26d51325 100644 --- a/app/db/tables.ts +++ b/app/db/tables.ts @@ -1305,6 +1305,7 @@ export interface UserReport { reporterUserId: number; category: UserReportCategory; description: string; + matchId: number | null; createdAt: Generated; } diff --git a/app/features/sendouq-match/SQMatchRepository.server.ts b/app/features/sendouq-match/SQMatchRepository.server.ts index 14784b490..a647d23ce 100644 --- a/app/features/sendouq-match/SQMatchRepository.server.ts +++ b/app/features/sendouq-match/SQMatchRepository.server.ts @@ -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") diff --git a/app/features/user-page/routes/u.$identifier.admin.tsx b/app/features/user-page/routes/u.$identifier.admin.tsx index 613076606..a3a7be78f 100644 --- a/app/features/user-page/routes/u.$identifier.admin.tsx +++ b/app/features/user-page/routes/u.$identifier.admin.tsx @@ -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)}

{report.description}

+ {report.matchId !== null ? ( + + SendouQ match #{report.matchId} + + ) : null} ))} diff --git a/app/features/user-report/UserReportRepository.server.ts b/app/features/user-report/UserReportRepository.server.ts index 2ddf8788a..6f4f43108 100644 --- a/app/features/user-report/UserReportRepository.server.ts +++ b/app/features/user-report/UserReportRepository.server.ts @@ -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", diff --git a/app/features/user-report/components/ReportUserDialog.tsx b/app/features/user-report/components/ReportUserDialog.tsx index 05f2b2195..b6c39061a 100644 --- a/app/features/user-report/components/ReportUserDialog.tsx +++ b/app/features/user-report/components/ReportUserDialog.tsx @@ -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 ( { toastQueue.add( { message: "Report sent to the staff", variant: "success" }, @@ -42,6 +47,7 @@ export function ReportUserDialog({ <> + {t("user:card.report.falseReportsWarning")} @@ -51,3 +57,17 @@ export function ReportUserDialog({ ); } + +/** + * 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; +} diff --git a/app/features/user-report/core/discord-webhook.server.ts b/app/features/user-report/core/discord-webhook.server.ts index 5b29db23c..d003db9bc 100644 --- a/app/features/user-report/core/discord-webhook.server.ts +++ b/app/features/user-report/core/discord-webhook.server.ts @@ -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}`, diff --git a/app/features/user-report/routes/user-report.$id.ts b/app/features/user-report/routes/user-report.$id.ts index b9dc603da..05fdd2e1d 100644 --- a/app/features/user-report/routes/user-report.$id.ts +++ b/app/features/user-report/routes/user-report.$id.ts @@ -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, }); diff --git a/app/features/user-report/user-report-constants.ts b/app/features/user-report/user-report-constants.ts index 854a51bc6..cb370fbdb 100644 --- a/app/features/user-report/user-report-constants.ts +++ b/app/features/user-report/user-report-constants.ts @@ -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. */ diff --git a/app/features/user-report/user-report-schemas.server.ts b/app/features/user-report/user-report-schemas.server.ts new file mode 100644 index 000000000..7912f7ef3 --- /dev/null +++ b/app/features/user-report/user-report-schemas.server.ts @@ -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) + .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)), +}); diff --git a/app/features/user-report/user-report-schemas.ts b/app/features/user-report/user-report-schemas.ts index f37ae1b5e..eaab7fa0c 100644 --- a/app/features/user-report/user-report-schemas.ts +++ b/app/features/user-report/user-report-schemas.ts @@ -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({ diff --git a/db-test.sqlite3 b/db-test.sqlite3 index 544d6e6aa..b9c9f2642 100644 Binary files a/db-test.sqlite3 and b/db-test.sqlite3 differ diff --git a/e2e/seeds/db-seed-AB_RR.sqlite3 b/e2e/seeds/db-seed-AB_RR.sqlite3 index 4dd164299..7c6142552 100644 Binary files a/e2e/seeds/db-seed-AB_RR.sqlite3 and b/e2e/seeds/db-seed-AB_RR.sqlite3 differ diff --git a/e2e/seeds/db-seed-DEFAULT.sqlite3 b/e2e/seeds/db-seed-DEFAULT.sqlite3 index 3ae87f832..e47c64457 100644 Binary files a/e2e/seeds/db-seed-DEFAULT.sqlite3 and b/e2e/seeds/db-seed-DEFAULT.sqlite3 differ diff --git a/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 b/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 index 0aff3caf9..c13a1715d 100644 Binary files a/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 and b/e2e/seeds/db-seed-FINALIZED_BRACKET.sqlite3 differ diff --git a/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 b/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 index 90b0a9a9e..f1831888d 100644 Binary files a/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 and b/e2e/seeds/db-seed-IN_SQ_MATCH.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 b/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 index 77835c937..9cc25113f 100644 Binary files a/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 and b/e2e/seeds/db-seed-NO_SCRIMS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 b/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 index ebd927897..91ca574fc 100644 Binary files a/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 and b/e2e/seeds/db-seed-NO_SQ_GROUPS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 b/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 index 354681dbd..3342d54ad 100644 Binary files a/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 and b/e2e/seeds/db-seed-NO_TOURNAMENT_TEAMS.sqlite3 differ diff --git a/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 b/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 index 25d77c250..07fa928a8 100644 Binary files a/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 and b/e2e/seeds/db-seed-NZAP_IN_TEAM.sqlite3 differ diff --git a/e2e/seeds/db-seed-REG_OPEN.sqlite3 b/e2e/seeds/db-seed-REG_OPEN.sqlite3 index 7c9388c93..421277bb0 100644 Binary files a/e2e/seeds/db-seed-REG_OPEN.sqlite3 and b/e2e/seeds/db-seed-REG_OPEN.sqlite3 differ diff --git a/e2e/seeds/db-seed-SMALL_SOS.sqlite3 b/e2e/seeds/db-seed-SMALL_SOS.sqlite3 index f0503db62..9b828f729 100644 Binary files a/e2e/seeds/db-seed-SMALL_SOS.sqlite3 and b/e2e/seeds/db-seed-SMALL_SOS.sqlite3 differ diff --git a/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 b/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 index 664a0c498..ad84205a0 100644 Binary files a/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 and b/e2e/seeds/db-seed-TEAM_MAP_PREFS.sqlite3 differ diff --git a/e2e/user-report.spec.ts b/e2e/user-report.spec.ts index 152f9438f..b3b09570e 100644 --- a/e2e/user-report.spec.ts +++ b/e2e/user-report.spec.ts @@ -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); + }); }); diff --git a/locales/da/forms.json b/locales/da/forms.json index c709b1c60..b99f635d4 100644 --- a/locales/da/forms.json +++ b/locales/da/forms.json @@ -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": "", diff --git a/locales/de/forms.json b/locales/de/forms.json index 892e0f712..785810a49 100644 --- a/locales/de/forms.json +++ b/locales/de/forms.json @@ -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": "", diff --git a/locales/en/forms.json b/locales/en/forms.json index ace9e0aba..382350f29 100644 --- a/locales/en/forms.json +++ b/locales/en/forms.json @@ -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", diff --git a/locales/es-ES/forms.json b/locales/es-ES/forms.json index ef5e151e0..612ffc342 100644 --- a/locales/es-ES/forms.json +++ b/locales/es-ES/forms.json @@ -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": "", diff --git a/locales/es-US/forms.json b/locales/es-US/forms.json index ec89ed166..ab0e6e34b 100644 --- a/locales/es-US/forms.json +++ b/locales/es-US/forms.json @@ -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": "", diff --git a/locales/fr-CA/forms.json b/locales/fr-CA/forms.json index 1276e72d4..8ef66b704 100644 --- a/locales/fr-CA/forms.json +++ b/locales/fr-CA/forms.json @@ -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": "", diff --git a/locales/fr-EU/forms.json b/locales/fr-EU/forms.json index bc2df2a18..78d63208a 100644 --- a/locales/fr-EU/forms.json +++ b/locales/fr-EU/forms.json @@ -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": "", diff --git a/locales/he/forms.json b/locales/he/forms.json index 96a6d662f..1565d5965 100644 --- a/locales/he/forms.json +++ b/locales/he/forms.json @@ -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": "", diff --git a/locales/it/forms.json b/locales/it/forms.json index 3ba3589ef..e0ffaf2e5 100644 --- a/locales/it/forms.json +++ b/locales/it/forms.json @@ -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": "", diff --git a/locales/ja/forms.json b/locales/ja/forms.json index aaaaa6032..4852c4235 100644 --- a/locales/ja/forms.json +++ b/locales/ja/forms.json @@ -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": "", diff --git a/locales/ko/forms.json b/locales/ko/forms.json index 7ac716307..8f16d366a 100644 --- a/locales/ko/forms.json +++ b/locales/ko/forms.json @@ -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": "", diff --git a/locales/nl/forms.json b/locales/nl/forms.json index 136f6e47b..46d64bacb 100644 --- a/locales/nl/forms.json +++ b/locales/nl/forms.json @@ -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": "", diff --git a/locales/pl/forms.json b/locales/pl/forms.json index 714073238..5e5b79066 100644 --- a/locales/pl/forms.json +++ b/locales/pl/forms.json @@ -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": "", diff --git a/locales/pt-BR/forms.json b/locales/pt-BR/forms.json index 11fc45b1e..8bd92f934 100644 --- a/locales/pt-BR/forms.json +++ b/locales/pt-BR/forms.json @@ -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": "", diff --git a/locales/ru/forms.json b/locales/ru/forms.json index c16da907b..a8912aaf7 100644 --- a/locales/ru/forms.json +++ b/locales/ru/forms.json @@ -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": "", diff --git a/locales/zh/forms.json b/locales/zh/forms.json index 7186bbf6f..890507437 100644 --- a/locales/zh/forms.json +++ b/locales/zh/forms.json @@ -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": "", diff --git a/migrations/158-user-report-match-id.js b/migrations/158-user-report-match-id.js new file mode 100644 index 000000000..2dbc2a82f --- /dev/null +++ b/migrations/158-user-report-match-id.js @@ -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"); + })(); +}