From c11d8ef9f76a906cd2ca810f76ece3c1c0859669 Mon Sep 17 00:00:00 2001 From: Kalle <38327916+Sendouc@users.noreply.github.com> Date: Sun, 28 Apr 2024 11:13:13 +0300 Subject: [PATCH] Move calendar new action to different file --- .../calendar/actions/calendar.new.server.ts | 256 ++++++++++++++++++ .../calendar/calendar-schemas.server.ts | 130 --------- .../calendar/calendar-utils.server.ts | 2 +- app/features/calendar/routes/calendar.new.tsx | 138 +--------- 4 files changed, 269 insertions(+), 257 deletions(-) create mode 100644 app/features/calendar/actions/calendar.new.server.ts delete mode 100644 app/features/calendar/calendar-schemas.server.ts diff --git a/app/features/calendar/actions/calendar.new.server.ts b/app/features/calendar/actions/calendar.new.server.ts new file mode 100644 index 000000000..8b0c40d7f --- /dev/null +++ b/app/features/calendar/actions/calendar.new.server.ts @@ -0,0 +1,256 @@ +import type { ActionFunction } from "@remix-run/node"; +import { redirect } from "@remix-run/node"; +import { z } from "zod"; +import { CALENDAR_EVENT } from "~/constants"; +import type { CalendarEventTag } from "~/db/types"; +import { requireUser } from "~/features/auth/core/user.server"; +import * as CalendarRepository from "~/features/calendar/CalendarRepository.server"; +import { MapPool } from "~/features/map-list-generator/core/map-pool"; +import { tournamentFromDB } from "~/features/tournament-bracket/core/Tournament.server"; +import { + FORMATS_SHORT, + TOURNAMENT, +} from "~/features/tournament/tournament-constants"; +import { rankedModesShort } from "~/modules/in-game-lists/modes"; +import { canEditCalendarEvent } from "~/permissions"; +import { + databaseTimestampToDate, + dateToDatabaseTimestamp, +} from "~/utils/dates"; +import { + badRequestIfFalsy, + parseRequestFormData, + validate, +} from "~/utils/remix"; +import { calendarEventPage } from "~/utils/urls"; +import { + actualNumber, + checkboxValueToBoolean, + date, + falsyToNull, + id, + processMany, + removeDuplicates, + safeJSONParse, + toArray, +} from "~/utils/zod"; +import { canAddNewEvent, regClosesAtDate } from "../calendar-utils"; +import { + canCreateTournament, + formValuesToBracketProgression, +} from "../calendar-utils.server"; +import { REG_CLOSES_AT_OPTIONS } from "../calendar-constants"; +import { calendarEventMaxDate, calendarEventMinDate } from "../calendar-utils"; + +export const action: ActionFunction = async ({ request }) => { + const user = await requireUser(request); + const data = await parseRequestFormData({ + request, + schema: newCalendarEventActionSchema, + parseAsync: true, + }); + + validate(canAddNewEvent(user), "Not authorized", 401); + + const startTimes = data.date.map((date) => dateToDatabaseTimestamp(date)); + const commonArgs = { + name: data.name, + description: data.description, + rules: data.rules, + startTimes, + bracketUrl: data.bracketUrl, + discordInviteCode: data.discordInviteCode, + tags: data.tags + ? data.tags + .sort( + (a, b) => + CALENDAR_EVENT.TAGS.indexOf(a as CalendarEventTag) - + CALENDAR_EVENT.TAGS.indexOf(b as CalendarEventTag), + ) + .join(",") + : data.tags, + badges: data.badges ?? [], + toToolsEnabled: canCreateTournament(user) ? Number(data.toToolsEnabled) : 0, + toToolsMode: + rankedModesShort.find((mode) => mode === data.toToolsMode) ?? null, + bracketProgression: formValuesToBracketProgression(data), + teamsPerGroup: data.teamsPerGroup ?? undefined, + thirdPlaceMatch: data.thirdPlaceMatch ?? undefined, + isRanked: data.isRanked ?? undefined, + enableNoScreenToggle: data.enableNoScreenToggle ?? undefined, + autoCheckInAll: data.autoCheckInAll ?? undefined, + autonomousSubs: data.autonomousSubs ?? undefined, + swissGroupCount: data.swissGroupCount ?? undefined, + swissRoundCount: data.swissRoundCount ?? undefined, + tournamentToCopyId: data.tournamentToCopyId, + regClosesAt: data.regClosesAt + ? dateToDatabaseTimestamp( + regClosesAtDate({ + startTime: databaseTimestampToDate(startTimes[0]), + closesAt: data.regClosesAt, + }), + ) + : undefined, + }; + validate( + !commonArgs.toToolsEnabled || commonArgs.bracketProgression, + "Bracket progression must be set for tournaments", + ); + + const deserializedMaps = (() => { + if (!data.pool) return; + + return MapPool.toDbList(data.pool); + })(); + + if (data.eventToEditId) { + const eventToEdit = badRequestIfFalsy( + await CalendarRepository.findById({ id: data.eventToEditId }), + ); + if (eventToEdit.tournamentId) { + const tournament = await tournamentFromDB({ + tournamentId: eventToEdit.tournamentId, + user, + }); + validate(!tournament.hasStarted, "Tournament has already started", 400); + } + validate( + canEditCalendarEvent({ user, event: eventToEdit }), + "Not authorized", + 401, + ); + + await CalendarRepository.update({ + eventId: data.eventToEditId, + mapPoolMaps: deserializedMaps, + ...commonArgs, + }); + + throw redirect(calendarEventPage(data.eventToEditId)); + } else { + const mapPickingStyle = () => { + if (data.toToolsMode === "TO") return "TO" as const; + if (data.toToolsMode) return `AUTO_${data.toToolsMode}` as const; + + return "AUTO_ALL" as const; + }; + const createdEventId = await CalendarRepository.create({ + authorId: user.id, + mapPoolMaps: deserializedMaps, + isFullTournament: data.toToolsEnabled, + mapPickingStyle: mapPickingStyle(), + ...commonArgs, + }); + + throw redirect(calendarEventPage(createdEventId)); + } +}; + +export const newCalendarEventActionSchema = z + .object({ + eventToEditId: z.preprocess(actualNumber, id.nullish()), + tournamentToCopyId: z.preprocess(actualNumber, id.nullish()), + name: z + .string() + .min(CALENDAR_EVENT.NAME_MIN_LENGTH) + .max(CALENDAR_EVENT.NAME_MAX_LENGTH), + description: z.preprocess( + falsyToNull, + z.string().max(CALENDAR_EVENT.DESCRIPTION_MAX_LENGTH).nullable(), + ), + rules: z.preprocess( + falsyToNull, + z.string().max(CALENDAR_EVENT.RULES_MAX_LENGTH).nullable(), + ), + date: z.preprocess( + toArray, + z + .array( + z.preprocess( + date, + z.date().min(calendarEventMinDate()).max(calendarEventMaxDate()), + ), + ) + .min(1) + .max(CALENDAR_EVENT.MAX_AMOUNT_OF_DATES), + ), + bracketUrl: z + .string() + .url() + .max(CALENDAR_EVENT.BRACKET_URL_MAX_LENGTH) + .default("https://sendou.ink"), + discordInviteCode: z.preprocess( + falsyToNull, + z.string().max(CALENDAR_EVENT.DISCORD_INVITE_CODE_MAX_LENGTH).nullable(), + ), + tags: z.preprocess( + processMany(safeJSONParse, removeDuplicates), + z + .array( + z + .string() + .refine((val) => + CALENDAR_EVENT.TAGS.includes(val as CalendarEventTag), + ), + ) + .nullable(), + ), + badges: z.preprocess( + processMany(safeJSONParse, removeDuplicates), + z.array(id).nullable(), + ), + pool: z.string().optional(), + toToolsEnabled: z.preprocess(checkboxValueToBoolean, z.boolean()), + toToolsMode: z.enum(["ALL", "TO", "SZ", "TC", "RM", "CB"]).optional(), + isRanked: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()), + regClosesAt: z.enum(REG_CLOSES_AT_OPTIONS).nullish(), + enableNoScreenToggle: z.preprocess( + checkboxValueToBoolean, + z.boolean().nullish(), + ), + autonomousSubs: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()), + // + // tournament format related fields + // + format: z.enum(FORMATS_SHORT).nullish(), + withUndergroundBracket: z.preprocess(checkboxValueToBoolean, z.boolean()), + thirdPlaceMatch: z.preprocess( + checkboxValueToBoolean, + z.boolean().nullish(), + ), + autoCheckInAll: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()), + teamsPerGroup: z.coerce + .number() + .min(TOURNAMENT.MIN_GROUP_SIZE) + .max(TOURNAMENT.MAX_GROUP_SIZE) + .nullish(), + swissGroupCount: z.coerce.number().int().positive().nullish(), + swissRoundCount: z.coerce.number().int().positive().nullish(), + followUpBrackets: z.preprocess( + safeJSONParse, + z + .array( + z.object({ + name: z.string(), + placements: z.array(z.number()), + }), + ) + .min(1) + .nullish(), + ), + }) + .refine( + async (schema) => { + if (schema.eventToEditId) { + const eventToEdit = await CalendarRepository.findById({ + id: schema.eventToEditId, + }); + return schema.date.length === 1 || !eventToEdit?.tournamentId; + } else { + return schema.date.length === 1 || !schema.toToolsEnabled; + } + }, + { + message: "Tournament must have exactly one date", + }, + ); diff --git a/app/features/calendar/calendar-schemas.server.ts b/app/features/calendar/calendar-schemas.server.ts deleted file mode 100644 index 9713fcf2b..000000000 --- a/app/features/calendar/calendar-schemas.server.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { z } from "zod"; -import { CALENDAR_EVENT } from "~/constants"; -import type { CalendarEventTag } from "~/db/types"; -import * as CalendarRepository from "~/features/calendar/CalendarRepository.server"; -import { - FORMATS_SHORT, - TOURNAMENT, -} from "~/features/tournament/tournament-constants"; -import { - actualNumber, - checkboxValueToBoolean, - date, - falsyToNull, - id, - processMany, - removeDuplicates, - safeJSONParse, - toArray, -} from "~/utils/zod"; -import { calendarEventMaxDate, calendarEventMinDate } from "./calendar-utils"; -import { REG_CLOSES_AT_OPTIONS } from "./calendar-constants"; - -export const newCalendarEventActionSchema = z - .object({ - eventToEditId: z.preprocess(actualNumber, id.nullish()), - tournamentToCopyId: z.preprocess(actualNumber, id.nullish()), - name: z - .string() - .min(CALENDAR_EVENT.NAME_MIN_LENGTH) - .max(CALENDAR_EVENT.NAME_MAX_LENGTH), - description: z.preprocess( - falsyToNull, - z.string().max(CALENDAR_EVENT.DESCRIPTION_MAX_LENGTH).nullable(), - ), - rules: z.preprocess( - falsyToNull, - z.string().max(CALENDAR_EVENT.RULES_MAX_LENGTH).nullable(), - ), - date: z.preprocess( - toArray, - z - .array( - z.preprocess( - date, - z.date().min(calendarEventMinDate()).max(calendarEventMaxDate()), - ), - ) - .min(1) - .max(CALENDAR_EVENT.MAX_AMOUNT_OF_DATES), - ), - bracketUrl: z - .string() - .url() - .max(CALENDAR_EVENT.BRACKET_URL_MAX_LENGTH) - .default("https://sendou.ink"), - discordInviteCode: z.preprocess( - falsyToNull, - z.string().max(CALENDAR_EVENT.DISCORD_INVITE_CODE_MAX_LENGTH).nullable(), - ), - tags: z.preprocess( - processMany(safeJSONParse, removeDuplicates), - z - .array( - z - .string() - .refine((val) => - CALENDAR_EVENT.TAGS.includes(val as CalendarEventTag), - ), - ) - .nullable(), - ), - badges: z.preprocess( - processMany(safeJSONParse, removeDuplicates), - z.array(id).nullable(), - ), - pool: z.string().optional(), - toToolsEnabled: z.preprocess(checkboxValueToBoolean, z.boolean()), - toToolsMode: z.enum(["ALL", "TO", "SZ", "TC", "RM", "CB"]).optional(), - isRanked: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()), - regClosesAt: z.enum(REG_CLOSES_AT_OPTIONS).nullish(), - enableNoScreenToggle: z.preprocess( - checkboxValueToBoolean, - z.boolean().nullish(), - ), - autonomousSubs: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()), - // - // tournament format related fields - // - format: z.enum(FORMATS_SHORT).nullish(), - withUndergroundBracket: z.preprocess(checkboxValueToBoolean, z.boolean()), - thirdPlaceMatch: z.preprocess( - checkboxValueToBoolean, - z.boolean().nullish(), - ), - autoCheckInAll: z.preprocess(checkboxValueToBoolean, z.boolean().nullish()), - teamsPerGroup: z.coerce - .number() - .min(TOURNAMENT.MIN_GROUP_SIZE) - .max(TOURNAMENT.MAX_GROUP_SIZE) - .nullish(), - swissGroupCount: z.coerce.number().int().positive().nullish(), - swissRoundCount: z.coerce.number().int().positive().nullish(), - followUpBrackets: z.preprocess( - safeJSONParse, - z - .array( - z.object({ - name: z.string(), - placements: z.array(z.number()), - }), - ) - .min(1) - .nullish(), - ), - }) - .refine( - async (schema) => { - if (schema.eventToEditId) { - const eventToEdit = await CalendarRepository.findById({ - id: schema.eventToEditId, - }); - return schema.date.length === 1 || !eventToEdit?.tournamentId; - } else { - return schema.date.length === 1 || !schema.toToolsEnabled; - } - }, - { - message: "Tournament must have exactly one date", - }, - ); diff --git a/app/features/calendar/calendar-utils.server.ts b/app/features/calendar/calendar-utils.server.ts index 904f2506e..94ebdd24f 100644 --- a/app/features/calendar/calendar-utils.server.ts +++ b/app/features/calendar/calendar-utils.server.ts @@ -2,8 +2,8 @@ import type { z } from "zod"; import { isAdmin } from "~/permissions"; import type { TournamentSettings } from "~/db/tables"; import { BRACKET_NAMES } from "../tournament/tournament-constants"; -import type { newCalendarEventActionSchema } from "./calendar-schemas.server"; import { validateFollowUpBrackets } from "./calendar-utils"; +import type { newCalendarEventActionSchema } from "./actions/calendar.new.server"; const usersWithTournamentPerms = process.env["TOURNAMENT_PERMS"]?.split(",").map(Number) ?? []; diff --git a/app/features/calendar/routes/calendar.new.tsx b/app/features/calendar/routes/calendar.new.tsx index 04d1ade02..444081194 100644 --- a/app/features/calendar/routes/calendar.new.tsx +++ b/app/features/calendar/routes/calendar.new.tsx @@ -1,5 +1,4 @@ import type { - ActionFunction, LoaderFunctionArgs, MetaFunction, SerializeFrom, @@ -20,6 +19,7 @@ import { Input } from "~/components/Input"; import { Label } from "~/components/Label"; import { Main } from "~/components/Main"; import { MapPoolSelector } from "~/components/MapPoolSelector"; +import { Placement } from "~/components/Placement"; import { RequiredHiddenInput } from "~/components/RequiredHiddenInput"; import { SubmitButton } from "~/components/SubmitButton"; import { Toggle } from "~/components/Toggle"; @@ -40,49 +40,39 @@ import { import { useIsMounted } from "~/hooks/useIsMounted"; import { i18next } from "~/modules/i18n/i18next.server"; import type { RankedModeShort } from "~/modules/in-game-lists"; -import { rankedModesShort } from "~/modules/in-game-lists/modes"; import { canEditCalendarEvent } from "~/permissions"; import { isDefined, nullFilledArray } from "~/utils/arrays"; import { databaseTimestampToDate, - dateToDatabaseTimestamp, getDateAtNextFullHour, getDateWithHoursOffset, } from "~/utils/dates"; -import { - badRequestIfFalsy, - parseRequestFormData, - validate, - type SendouRouteHandle, -} from "~/utils/remix"; +import { validate, type SendouRouteHandle } from "~/utils/remix"; import { makeTitle, pathnameFromPotentialURL } from "~/utils/strings"; -import { calendarEventPage, tournamentBracketsPage } from "~/utils/urls"; -import { newCalendarEventActionSchema } from "../calendar-schemas.server"; +import { tournamentBracketsPage } from "~/utils/urls"; +import { + REG_CLOSES_AT_OPTIONS, + type RegClosesAtOption, +} from "../calendar-constants"; +import type { FollowUpBracket } from "../calendar-types"; import { bracketProgressionToShortTournamentFormat, calendarEventMaxDate, calendarEventMinDate, canAddNewEvent, datesToRegClosesAt, - regClosesAtDate, regClosesAtToDisplayName, validateFollowUpBrackets, } from "../calendar-utils"; -import { - canCreateTournament, - formValuesToBracketProgression, -} from "../calendar-utils.server"; +import { canCreateTournament } from "../calendar-utils.server"; import { Tags } from "../components/Tags"; -import { Placement } from "~/components/Placement"; -import type { FollowUpBracket } from "../calendar-types"; -import { - REG_CLOSES_AT_OPTIONS, - type RegClosesAtOption, -} from "../calendar-constants"; import "~/styles/calendar-new.css"; import "~/styles/maps.css"; +import { action } from "../actions/calendar.new.server"; +export { action }; + export const meta: MetaFunction = (args) => { const data = args.data as SerializeFrom | null; @@ -91,110 +81,6 @@ export const meta: MetaFunction = (args) => { return [{ title: data.title }]; }; -export const action: ActionFunction = async ({ request }) => { - const user = await requireUser(request); - const data = await parseRequestFormData({ - request, - schema: newCalendarEventActionSchema, - parseAsync: true, - }); - - validate(canAddNewEvent(user), "Not authorized", 401); - - const startTimes = data.date.map((date) => dateToDatabaseTimestamp(date)); - const commonArgs = { - name: data.name, - description: data.description, - rules: data.rules, - startTimes, - bracketUrl: data.bracketUrl, - discordInviteCode: data.discordInviteCode, - tags: data.tags - ? data.tags - .sort( - (a, b) => - CALENDAR_EVENT.TAGS.indexOf(a as CalendarEventTag) - - CALENDAR_EVENT.TAGS.indexOf(b as CalendarEventTag), - ) - .join(",") - : data.tags, - badges: data.badges ?? [], - toToolsEnabled: canCreateTournament(user) ? Number(data.toToolsEnabled) : 0, - toToolsMode: - rankedModesShort.find((mode) => mode === data.toToolsMode) ?? null, - bracketProgression: formValuesToBracketProgression(data), - teamsPerGroup: data.teamsPerGroup ?? undefined, - thirdPlaceMatch: data.thirdPlaceMatch ?? undefined, - isRanked: data.isRanked ?? undefined, - enableNoScreenToggle: data.enableNoScreenToggle ?? undefined, - autoCheckInAll: data.autoCheckInAll ?? undefined, - autonomousSubs: data.autonomousSubs ?? undefined, - swissGroupCount: data.swissGroupCount ?? undefined, - swissRoundCount: data.swissRoundCount ?? undefined, - tournamentToCopyId: data.tournamentToCopyId, - regClosesAt: data.regClosesAt - ? dateToDatabaseTimestamp( - regClosesAtDate({ - startTime: databaseTimestampToDate(startTimes[0]), - closesAt: data.regClosesAt, - }), - ) - : undefined, - }; - validate( - !commonArgs.toToolsEnabled || commonArgs.bracketProgression, - "Bracket progression must be set for tournaments", - ); - - const deserializedMaps = (() => { - if (!data.pool) return; - - return MapPool.toDbList(data.pool); - })(); - - if (data.eventToEditId) { - const eventToEdit = badRequestIfFalsy( - await CalendarRepository.findById({ id: data.eventToEditId }), - ); - if (eventToEdit.tournamentId) { - const tournament = await tournamentFromDB({ - tournamentId: eventToEdit.tournamentId, - user, - }); - validate(!tournament.hasStarted, "Tournament has already started", 400); - } - validate( - canEditCalendarEvent({ user, event: eventToEdit }), - "Not authorized", - 401, - ); - - await CalendarRepository.update({ - eventId: data.eventToEditId, - mapPoolMaps: deserializedMaps, - ...commonArgs, - }); - - throw redirect(calendarEventPage(data.eventToEditId)); - } else { - const mapPickingStyle = () => { - if (data.toToolsMode === "TO") return "TO" as const; - if (data.toToolsMode) return `AUTO_${data.toToolsMode}` as const; - - return "AUTO_ALL" as const; - }; - const createdEventId = await CalendarRepository.create({ - authorId: user.id, - mapPoolMaps: deserializedMaps, - isFullTournament: data.toToolsEnabled, - mapPickingStyle: mapPickingStyle(), - ...commonArgs, - }); - - throw redirect(calendarEventPage(createdEventId)); - } -}; - export const handle: SendouRouteHandle = { i18n: ["calendar", "game-misc"], };