mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-03-21 18:04:39 -05:00
Scrim improvements (#2365)
* Initial * wip * wip * Finish? * remove comment
This commit is contained in:
parent
61ecaa252f
commit
7901ee8cab
13
.github/copilot-instructions.md
vendored
13
.github/copilot-instructions.md
vendored
|
|
@ -1,6 +1,9 @@
|
|||
## General
|
||||
|
||||
- for new code only rarely use comments, prefer descriptive variable and function names
|
||||
- only rarely use comments, prefer descriptive variable and function names (leave existing comments as is)
|
||||
- if you encounter an existing TODO comment assume it is there for a reason and do not remove it
|
||||
- for running scripts `npm` is used
|
||||
- all the imports should be at the top of the file
|
||||
|
||||
## Commands
|
||||
|
||||
|
|
@ -27,6 +30,7 @@
|
|||
- split bigger components into smaller ones
|
||||
- one file can have many components
|
||||
- all texts should be provided translations via the i18next library's `useTranslations` hook's `t` function
|
||||
- instead of `&&` operator for conditional rendering, use the ternary operator
|
||||
|
||||
## Styling
|
||||
|
||||
|
|
@ -39,3 +43,10 @@
|
|||
|
||||
- database is Sqlite3 used with the Kysely library
|
||||
- database code should only be written in Repository files
|
||||
- down migrations are not needed, only up migrations
|
||||
- every database id is of type number
|
||||
|
||||
## Playwright
|
||||
|
||||
- `page.goto` is forbidden, use the `navigate` function to do a page navigation
|
||||
- to submit a form you use the `submit` function
|
||||
|
|
|
|||
|
|
@ -18,10 +18,12 @@ export default function TimePopover({
|
|||
month: "long",
|
||||
},
|
||||
underline = true,
|
||||
className,
|
||||
}: {
|
||||
time: Date;
|
||||
options?: Intl.DateTimeFormatOptions;
|
||||
underline?: boolean;
|
||||
className?: string;
|
||||
}) {
|
||||
const { i18n } = useTranslation();
|
||||
|
||||
|
|
@ -49,6 +51,7 @@ export default function TimePopover({
|
|||
type="button"
|
||||
ref={triggerRef}
|
||||
className={clsx(
|
||||
className,
|
||||
"clickable text-only-button",
|
||||
underline ? "dotted" : "",
|
||||
)}
|
||||
|
|
@ -66,7 +69,7 @@ export default function TimePopover({
|
|||
>
|
||||
<Dialog>
|
||||
<div className="stack sm">
|
||||
<div className="text-center">
|
||||
<div className="text-center" suppressHydrationWarning>
|
||||
{time.toLocaleTimeString(i18n.language, {
|
||||
timeZoneName: "long",
|
||||
hour: "numeric",
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ interface SendouDialogProps extends ModalOverlayProps {
|
|||
"aria-label"?: string;
|
||||
/** If true, the modal takes over the full screen with the content below hidden */
|
||||
isFullScreen?: boolean;
|
||||
/** If true, shows the close button even if onClose is not provided */
|
||||
showCloseButton?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -77,11 +79,12 @@ function DialogModal({
|
|||
heading,
|
||||
showHeading = true,
|
||||
className,
|
||||
showCloseButton: showCloseButtonProp,
|
||||
...rest
|
||||
}: Omit<SendouDialogProps, "trigger">) {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const showCloseButton = rest.onClose || rest.onCloseTo;
|
||||
const showCloseButton = showCloseButtonProp || rest.onClose || rest.onCloseTo;
|
||||
const onClose = () => {
|
||||
if (rest.onCloseTo) {
|
||||
navigate(rest.onCloseTo);
|
||||
|
|
|
|||
|
|
@ -15,12 +15,14 @@ export function SendouForm<T extends z.ZodTypeAny>({
|
|||
heading,
|
||||
children,
|
||||
cancelLink,
|
||||
submitButtonTestId,
|
||||
}: {
|
||||
schema: T;
|
||||
defaultValues?: DefaultValues<z.infer<T>>;
|
||||
heading?: string;
|
||||
children: React.ReactNode;
|
||||
cancelLink?: string;
|
||||
submitButtonTestId?: string;
|
||||
}) {
|
||||
const { t } = useTranslation(["common"]);
|
||||
const fetcher = useFetcher<any>();
|
||||
|
|
@ -56,7 +58,7 @@ export function SendouForm<T extends z.ZodTypeAny>({
|
|||
{heading ? <h1 className="text-lg">{heading}</h1> : null}
|
||||
{children}
|
||||
<div className="stack horizontal lg justify-between mt-6 w-full">
|
||||
<SubmitButton state={fetcher.state}>
|
||||
<SubmitButton state={fetcher.state} testId={submitButtonTestId}>
|
||||
{t("common:actions.submit")}
|
||||
</SubmitButton>
|
||||
{cancelLink ? (
|
||||
|
|
|
|||
|
|
@ -2295,22 +2295,29 @@ async function lfgPosts() {
|
|||
async function scrimPosts() {
|
||||
const allUsers = userIdsInRandomOrder(true);
|
||||
|
||||
const date = () => {
|
||||
// Only schedule admin's scrim at least 1 hour in the future, others can be 'now'
|
||||
const date = (isAdmin = false) => {
|
||||
if (isAdmin) {
|
||||
const randomFuture = faker.date.between({
|
||||
from: add(new Date(), { hours: 1 }),
|
||||
to: add(new Date(), { days: 7 }),
|
||||
});
|
||||
randomFuture.setMinutes(0);
|
||||
randomFuture.setSeconds(0);
|
||||
randomFuture.setMilliseconds(0);
|
||||
return dateToDatabaseTimestamp(randomFuture);
|
||||
}
|
||||
const isNow = faker.number.float(1) > 0.5;
|
||||
|
||||
if (isNow) {
|
||||
return databaseTimestampNow();
|
||||
}
|
||||
|
||||
const randomFuture = faker.date.between({
|
||||
from: new Date(),
|
||||
to: add(new Date(), { days: 7 }),
|
||||
});
|
||||
|
||||
randomFuture.setMinutes(0);
|
||||
randomFuture.setSeconds(0);
|
||||
randomFuture.setMilliseconds(0);
|
||||
|
||||
return dateToDatabaseTimestamp(randomFuture);
|
||||
};
|
||||
|
||||
|
|
@ -2355,7 +2362,6 @@ async function scrimPosts() {
|
|||
|
||||
for (let i = 0; i < 20; i++) {
|
||||
const divs = divRange();
|
||||
|
||||
await ScrimPostRepository.insert({
|
||||
at: date(),
|
||||
maxDiv: divs?.maxDiv,
|
||||
|
|
@ -2367,11 +2373,12 @@ async function scrimPosts() {
|
|||
: null,
|
||||
visibility: null,
|
||||
users: users(),
|
||||
managedByAnyone: true,
|
||||
});
|
||||
}
|
||||
|
||||
const adminPostId = await ScrimPostRepository.insert({
|
||||
at: date(),
|
||||
at: date(true), // admin's scrim is always at least 1 hour in the future
|
||||
text:
|
||||
faker.number.float(1) > 0.5
|
||||
? faker.lorem.sentences({ min: 1, max: 5 })
|
||||
|
|
@ -2380,6 +2387,7 @@ async function scrimPosts() {
|
|||
users: users()
|
||||
.map((u) => ({ ...u, isOwner: 0 }))
|
||||
.concat({ userId: ADMIN_ID, isOwner: 1 }),
|
||||
managedByAnyone: true,
|
||||
});
|
||||
await ScrimPostRepository.insertRequest({
|
||||
scrimPostId: adminPostId,
|
||||
|
|
|
|||
|
|
@ -948,6 +948,14 @@ export interface ScrimPost {
|
|||
chatCode: string;
|
||||
/** Refers to the team looking for the team (can also be a pick-up) */
|
||||
teamId: number | null;
|
||||
/** Indicates if anyone in the post can manage it */
|
||||
managedByAnyone: DBBoolean;
|
||||
/** When the scrim was canceled */
|
||||
canceledAt: number | null;
|
||||
/** User id who canceled the scrim */
|
||||
canceledByUserId: number | null;
|
||||
/** Reason for canceling the scrim */
|
||||
cancelReason: string | null;
|
||||
createdAt: GeneratedAlways<number>;
|
||||
updatedAt: Generated<number>;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { sub } from "date-fns";
|
|||
import type { Insertable } from "kysely";
|
||||
import { jsonArrayFrom, jsonBuildObject } from "kysely/helpers/sqlite";
|
||||
import type { Tables, TablesInsertable } from "~/db/tables";
|
||||
import { dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { databaseTimestampNow, dateToDatabaseTimestamp } from "~/utils/dates";
|
||||
import { shortNanoid } from "~/utils/id";
|
||||
import { COMMON_USER_FIELDS } from "~/utils/kysely.server";
|
||||
import { db } from "../../db/sql";
|
||||
|
|
@ -10,7 +10,7 @@ import invariant from "../../utils/invariant";
|
|||
import type { Unwrapped } from "../../utils/types";
|
||||
import type { AssociationVisibility } from "../associations/associations-types";
|
||||
import * as Scrim from "./core/Scrim";
|
||||
import type { ScrimPost } from "./scrims-types";
|
||||
import type { ScrimPost, ScrimPostUser } from "./scrims-types";
|
||||
import { getPostRequestCensor, parseLutiDiv } from "./scrims-utils";
|
||||
|
||||
type InsertArgs = Pick<
|
||||
|
|
@ -20,6 +20,7 @@ type InsertArgs = Pick<
|
|||
/** users related to the post other than the author */
|
||||
users: Array<Pick<Insertable<Tables["ScrimPostUser"]>, "userId" | "isOwner">>;
|
||||
visibility: AssociationVisibility | null;
|
||||
managedByAnyone: boolean;
|
||||
};
|
||||
|
||||
export function insert(args: InsertArgs) {
|
||||
|
|
@ -38,6 +39,7 @@ export function insert(args: InsertArgs) {
|
|||
text: args.text,
|
||||
visibility: args.visibility ? JSON.stringify(args.visibility) : null,
|
||||
chatCode: shortNanoid(),
|
||||
managedByAnyone: args.managedByAnyone ? 1 : 0,
|
||||
})
|
||||
.returning("id")
|
||||
.executeTakeFirstOrThrow();
|
||||
|
|
@ -101,6 +103,10 @@ const baseFindQuery = db
|
|||
"ScrimPost.maxDiv",
|
||||
"ScrimPost.minDiv",
|
||||
"ScrimPost.text",
|
||||
"ScrimPost.managedByAnyone",
|
||||
"ScrimPost.canceledAt",
|
||||
"ScrimPost.canceledByUserId",
|
||||
"ScrimPost.cancelReason",
|
||||
jsonBuildObject({
|
||||
name: eb.ref("Team.name"),
|
||||
customUrl: eb.ref("Team.customUrl"),
|
||||
|
|
@ -168,9 +174,34 @@ const mapDBRowToScrimPost = (
|
|||
? row.requests.filter((request) => request.isAccepted)
|
||||
: row.requests;
|
||||
|
||||
const ownerIds = row.users
|
||||
.filter((user) => user.isOwner)
|
||||
.map((user) => user.id);
|
||||
const users: ScrimPostUser[] = row.users.map((user) => ({
|
||||
...user,
|
||||
isOwner: Boolean(user.isOwner),
|
||||
}));
|
||||
|
||||
const ownerIds = users.filter((user) => user.isOwner).map((user) => user.id);
|
||||
const managerIds = row.managedByAnyone
|
||||
? users.map((user) => user.id)
|
||||
: ownerIds;
|
||||
|
||||
let canceled: ScrimPost["canceled"] = null;
|
||||
if (row.canceledAt && row.cancelReason) {
|
||||
let cancelingUser = users.find((u) => u.id === row.canceledByUserId);
|
||||
if (!cancelingUser) {
|
||||
const allRequestUsers = requests.flatMap((request) => request.users);
|
||||
const found = allRequestUsers.find((u) => u.id === row.canceledByUserId);
|
||||
if (found) {
|
||||
cancelingUser = { ...found, isOwner: Boolean(found.isOwner) };
|
||||
}
|
||||
}
|
||||
if (cancelingUser) {
|
||||
canceled = {
|
||||
at: row.canceledAt,
|
||||
byUser: cancelingUser,
|
||||
reason: row.cancelReason,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
id: row.id,
|
||||
|
|
@ -201,29 +232,23 @@ const mapDBRowToScrimPost = (
|
|||
avatarUrl: request.team.avatarUrl,
|
||||
}
|
||||
: null,
|
||||
users: request.users.map((user) => {
|
||||
return {
|
||||
...user,
|
||||
isVerified: false,
|
||||
isOwner: Boolean(user.isOwner),
|
||||
};
|
||||
}),
|
||||
users: request.users.map((user) => ({
|
||||
...user,
|
||||
isOwner: Boolean(user.isOwner),
|
||||
})),
|
||||
permissions: {
|
||||
CANCEL: request.users.map((u) => u.id),
|
||||
},
|
||||
};
|
||||
}),
|
||||
users: row.users.map((user) => {
|
||||
return {
|
||||
...user,
|
||||
isVerified: false,
|
||||
isOwner: Boolean(user.isOwner),
|
||||
};
|
||||
}),
|
||||
users,
|
||||
permissions: {
|
||||
MANAGE_REQUESTS: ownerIds,
|
||||
DELETE_POST: ownerIds,
|
||||
MANAGE_REQUESTS: managerIds,
|
||||
DELETE_POST: managerIds,
|
||||
CANCEL: managerIds.concat(requests.at(0)?.users.map((u) => u.id) ?? []),
|
||||
},
|
||||
managedByAnyone: Boolean(row.managedByAnyone),
|
||||
canceled,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
@ -268,3 +293,19 @@ export function deleteRequest(scrimPostRequestId: number) {
|
|||
.where("id", "=", scrimPostRequestId)
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function cancelScrim(
|
||||
id: number,
|
||||
{ userId, reason }: { userId: number; reason: string },
|
||||
) {
|
||||
await db
|
||||
.updateTable("ScrimPost")
|
||||
.set({
|
||||
canceledAt: databaseTimestampNow(),
|
||||
canceledByUserId: userId,
|
||||
cancelReason: reason,
|
||||
})
|
||||
.where("id", "=", id)
|
||||
.where("canceledAt", "is", null)
|
||||
.execute();
|
||||
}
|
||||
|
|
|
|||
37
app/features/scrims/actions/scrims.$id.server.ts
Normal file
37
app/features/scrims/actions/scrims.$id.server.ts
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
import type { ActionFunctionArgs } from "@remix-run/node";
|
||||
import { requirePermission } from "~/modules/permissions/guards.server";
|
||||
import {
|
||||
notFoundIfFalsy,
|
||||
parseParams,
|
||||
parseRequestPayload,
|
||||
} from "~/utils/remix.server";
|
||||
import { idObject } from "~/utils/zod";
|
||||
import { databaseTimestampToDate } from "../../../utils/dates";
|
||||
import { errorToast } from "../../../utils/remix.server";
|
||||
import { requireUser } from "../../auth/core/user.server";
|
||||
import * as ScrimPostRepository from "../ScrimPostRepository.server";
|
||||
import { cancelScrimSchema } from "../scrims-schemas";
|
||||
|
||||
export const action = async ({ request, params }: ActionFunctionArgs) => {
|
||||
const { id } = parseParams({ params, schema: idObject });
|
||||
const post = notFoundIfFalsy(await ScrimPostRepository.findById(id));
|
||||
|
||||
const user = await requireUser(request);
|
||||
const data = await parseRequestPayload({
|
||||
request,
|
||||
schema: cancelScrimSchema,
|
||||
});
|
||||
|
||||
requirePermission(post, "CANCEL", user);
|
||||
|
||||
if (databaseTimestampToDate(post.at) < new Date()) {
|
||||
errorToast("Cannot cancel a scrim that was already scheduled to start");
|
||||
}
|
||||
|
||||
await ScrimPostRepository.cancelScrim(id, {
|
||||
userId: user.id,
|
||||
reason: data.reason,
|
||||
});
|
||||
|
||||
return null;
|
||||
};
|
||||
|
|
@ -53,6 +53,7 @@ export const action = async ({ request }: ActionFunctionArgs) => {
|
|||
maxDiv: data.divs ? serializeLutiDiv(data.divs.max!) : null,
|
||||
minDiv: data.divs ? serializeLutiDiv(data.divs.min!) : null,
|
||||
text: data.postText,
|
||||
managedByAnyone: data.managedByAnyone,
|
||||
visibility:
|
||||
data.baseVisibility !== "PUBLIC"
|
||||
? {
|
||||
|
|
|
|||
|
|
@ -1,7 +1,17 @@
|
|||
import { Link, useLoaderData } from "@remix-run/react";
|
||||
import * as React from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import type { z } from "zod";
|
||||
import { Alert } from "~/components/Alert";
|
||||
import TimePopover from "~/components/TimePopover";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouDialog } from "~/components/elements/Dialog";
|
||||
import { SendouForm } from "~/components/form/SendouForm";
|
||||
import { TextAreaFormField } from "~/components/form/TextAreaFormField";
|
||||
import { SCRIM } from "~/features/scrims/scrims-constants";
|
||||
import { cancelScrimSchema } from "~/features/scrims/scrims-schemas";
|
||||
import { resolveRoomPass } from "~/features/tournament-bracket/tournament-bracket-utils";
|
||||
import { useHasPermission } from "~/modules/permissions/hooks";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { Avatar } from "../../../components/Avatar";
|
||||
import { Main } from "../../../components/Main";
|
||||
|
|
@ -12,10 +22,10 @@ import { ConnectedChat } from "../../chat/components/Chat";
|
|||
import * as Scrim from "../core/Scrim";
|
||||
import type { ScrimPost as ScrimPostType } from "../scrims-types";
|
||||
|
||||
import { action } from "../actions/scrims.$id.server";
|
||||
import { loader } from "../loaders/scrims.$id.server";
|
||||
export { loader };
|
||||
export { loader, action };
|
||||
|
||||
import TimePopover from "~/components/TimePopover";
|
||||
import styles from "./scrims.$id.module.css";
|
||||
|
||||
export const handle: SendouRouteHandle = {
|
||||
|
|
@ -23,12 +33,46 @@ export const handle: SendouRouteHandle = {
|
|||
};
|
||||
|
||||
export default function ScrimPage() {
|
||||
const { t } = useTranslation(["q"]);
|
||||
const { t } = useTranslation(["q", "scrims", "common"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
const allowedToCancel = useHasPermission(data.post, "CANCEL");
|
||||
const isCanceled = Boolean(data.post.canceled);
|
||||
const canCancel =
|
||||
allowedToCancel &&
|
||||
!isCanceled &&
|
||||
databaseTimestampToDate(data.post.at) > new Date();
|
||||
|
||||
return (
|
||||
<Main className="stack lg">
|
||||
<ScrimHeader />
|
||||
<div className="stack horizontal justify-between">
|
||||
<ScrimHeader />
|
||||
{canCancel && (
|
||||
<div>
|
||||
<SendouDialog
|
||||
trigger={
|
||||
<SendouButton size="small" variant="minimal-destructive">
|
||||
{t("common:actions.cancel")}
|
||||
</SendouButton>
|
||||
}
|
||||
heading={t("scrims:cancelModal.scrim.title")}
|
||||
showCloseButton
|
||||
>
|
||||
<CancelScrimForm />
|
||||
</SendouDialog>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{data.post.canceled && (
|
||||
<div className="mx-auto">
|
||||
<Alert variation="WARNING">
|
||||
{t("scrims:alert.canceled", {
|
||||
user: data.post.canceled.byUser.username,
|
||||
reason: data.post.canceled.reason,
|
||||
})}
|
||||
</Alert>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.groupsContainer}>
|
||||
<GroupCard group={data.post} side="ALPHA" />
|
||||
<GroupCard group={data.post.requests[0]} side="BRAVO" />
|
||||
|
|
@ -48,13 +92,34 @@ export default function ScrimPage() {
|
|||
);
|
||||
}
|
||||
|
||||
type FormFields = z.infer<typeof cancelScrimSchema>;
|
||||
|
||||
function CancelScrimForm() {
|
||||
const { t } = useTranslation(["scrims"]);
|
||||
|
||||
return (
|
||||
<SendouForm
|
||||
schema={cancelScrimSchema}
|
||||
defaultValues={{ reason: "" }}
|
||||
submitButtonTestId="cancel-scrim-submit"
|
||||
>
|
||||
<TextAreaFormField<FormFields>
|
||||
name="reason"
|
||||
label={t("cancelModal.scrim.reasonLabel")}
|
||||
maxLength={SCRIM.CANCEL_REASON_MAX_LENGTH}
|
||||
bottomText={t("scrims:cancelModal.scrim.reasonExplanation")}
|
||||
/>
|
||||
</SendouForm>
|
||||
);
|
||||
}
|
||||
|
||||
function ScrimHeader() {
|
||||
const { t } = useTranslation(["scrims"]);
|
||||
const data = useLoaderData<typeof loader>();
|
||||
|
||||
return (
|
||||
<div className="line-height-tight" data-testid="match-header">
|
||||
<h2 className="text-lg" suppressHydrationWarning>
|
||||
<h2 className="text-lg">
|
||||
<TimePopover
|
||||
time={databaseTimestampToDate(data.post.at)}
|
||||
options={{
|
||||
|
|
@ -65,6 +130,7 @@ function ScrimHeader() {
|
|||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
}}
|
||||
className="text-left"
|
||||
/>
|
||||
</h2>
|
||||
<div className="text-lighter text-xs font-bold">
|
||||
|
|
|
|||
|
|
@ -53,6 +53,14 @@
|
|||
fill: var(--theme-info);
|
||||
}
|
||||
|
||||
.postStatusCanceled {
|
||||
background-color: var(--theme-warning-transparent);
|
||||
}
|
||||
|
||||
.postStatusCanceled svg {
|
||||
fill: var(--theme-warning);
|
||||
}
|
||||
|
||||
.postFloatingActionCell {
|
||||
position: sticky;
|
||||
right: 0;
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import { Label } from "~/components/Label";
|
|||
import { DateFormField } from "~/components/form/DateFormField";
|
||||
import { SendouForm } from "~/components/form/SendouForm";
|
||||
import { TextAreaFormField } from "~/components/form/TextAreaFormField";
|
||||
import { ToggleFormField } from "~/components/form/ToggleFormField";
|
||||
import { nullFilledArray } from "~/utils/arrays";
|
||||
import type { SendouRouteHandle } from "~/utils/remix.server";
|
||||
import { FormMessage } from "../../../components/FormMessage";
|
||||
|
|
@ -58,6 +59,7 @@ export default function NewScrimPage() {
|
|||
SCRIM.MAX_PICKUP_SIZE_EXCLUDING_OWNER,
|
||||
) as unknown as number[],
|
||||
},
|
||||
managedByAnyone: true,
|
||||
}}
|
||||
>
|
||||
<WithFormField usersTeams={data.teams} />
|
||||
|
|
@ -80,6 +82,12 @@ export default function NewScrimPage() {
|
|||
name="postText"
|
||||
maxLength={MAX_SCRIM_POST_TEXT_LENGTH}
|
||||
/>
|
||||
|
||||
<ToggleFormField<FormFields>
|
||||
label={t("scrims:forms.managedByAnyone.title")}
|
||||
name="managedByAnyone"
|
||||
bottomText={t("scrims:forms.managedByAnyone.explanation")}
|
||||
/>
|
||||
</SendouForm>
|
||||
</Main>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { LinkButton } from "~/components/Button";
|
|||
import { Divider } from "~/components/Divider";
|
||||
import { FormWithConfirm } from "~/components/FormWithConfirm";
|
||||
import { Table } from "~/components/Table";
|
||||
import TimePopover from "~/components/TimePopover";
|
||||
import { SendouButton } from "~/components/elements/Button";
|
||||
import { SendouDialog } from "~/components/elements/Dialog";
|
||||
import { SendouPopover } from "~/components/elements/Popover";
|
||||
|
|
@ -50,7 +51,6 @@ import { action } from "../actions/scrims.server";
|
|||
import { loader } from "../loaders/scrims.server";
|
||||
export { loader, action };
|
||||
|
||||
import TimePopover from "~/components/TimePopover";
|
||||
import styles from "./scrims.module.css";
|
||||
|
||||
export type NewRequestFormFields = z.infer<typeof newRequestSchema>;
|
||||
|
|
@ -138,6 +138,7 @@ export default function ScrimsPage() {
|
|||
posts={data.posts.owned}
|
||||
showDeletePost
|
||||
showRequestRows
|
||||
showStatus
|
||||
/>
|
||||
),
|
||||
},
|
||||
|
|
@ -297,6 +298,7 @@ function ScrimsTable({
|
|||
);
|
||||
|
||||
const getStatus = (post: ScrimPost) => {
|
||||
if (post.canceled) return "CANCELED";
|
||||
if (post.requests.at(0)?.isAccepted) return "CONFIRMED";
|
||||
if (
|
||||
post.requests.some((r) => r.users.some((rUser) => user?.id === rUser.id))
|
||||
|
|
@ -457,6 +459,7 @@ function ScrimsTable({
|
|||
className={clsx(styles.postStatus, {
|
||||
[styles.postStatusConfirmed]: status === "CONFIRMED",
|
||||
[styles.postStatusPending]: status === "PENDING",
|
||||
[styles.postStatusCanceled]: status === "CANCELED",
|
||||
})}
|
||||
>
|
||||
{status === "CONFIRMED" ? (
|
||||
|
|
@ -469,6 +472,11 @@ function ScrimsTable({
|
|||
<ClockIcon /> {t("scrims:status.pending")}
|
||||
</>
|
||||
) : null}
|
||||
{status === "CANCELED" ? (
|
||||
<>
|
||||
<CrossIcon /> {t("scrims:status.canceled")}
|
||||
</>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
) : null}
|
||||
|
|
@ -515,7 +523,9 @@ function ScrimsTable({
|
|||
</SendouButton>
|
||||
}
|
||||
>
|
||||
{t("scrims:deleteModal.prevented")}
|
||||
{t("scrims:deleteModal.prevented", {
|
||||
username: owner.username,
|
||||
})}
|
||||
</SendouPopover>
|
||||
)}
|
||||
</td>
|
||||
|
|
@ -637,6 +647,7 @@ function RequestRow({
|
|||
</td>
|
||||
<td />
|
||||
<td />
|
||||
<td />
|
||||
<td className={styles.postFloatingActionCell}>
|
||||
{!request.isAccepted && canAccept ? (
|
||||
<FormWithConfirm
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ export const LUTI_DIVS = [
|
|||
export const SCRIM = {
|
||||
MAX_PICKUP_SIZE_EXCLUDING_OWNER: 5,
|
||||
MIN_MEMBERS_PER_TEAM: 4,
|
||||
CANCEL_REASON_MAX_LENGTH: 500,
|
||||
};
|
||||
|
||||
export const FF_SCRIMS_ENABLED = true;
|
||||
|
|
|
|||
|
|
@ -50,6 +50,10 @@ export const cancelRequestSchema = z.object({
|
|||
scrimPostRequestId: id,
|
||||
});
|
||||
|
||||
export const cancelScrimSchema = z.object({
|
||||
reason: z.string().trim().min(1).max(SCRIM.CANCEL_REASON_MAX_LENGTH),
|
||||
});
|
||||
|
||||
export const scrimsActionSchema = z.union([
|
||||
deletePostSchema,
|
||||
newRequestSchema,
|
||||
|
|
@ -140,6 +144,7 @@ export const scrimsNewActionSchema = z
|
|||
falsyToNull,
|
||||
z.string().max(MAX_SCRIM_POST_TEXT_LENGTH).nullable(),
|
||||
),
|
||||
managedByAnyone: z.boolean(),
|
||||
})
|
||||
.superRefine((post, ctx) => {
|
||||
if (
|
||||
|
|
|
|||
|
|
@ -24,7 +24,14 @@ export interface ScrimPost {
|
|||
permissions: {
|
||||
MANAGE_REQUESTS: number[];
|
||||
DELETE_POST: number[];
|
||||
CANCEL: number[];
|
||||
};
|
||||
managedByAnyone: boolean;
|
||||
canceled: {
|
||||
at: number;
|
||||
byUser: ScrimPostUser;
|
||||
reason: string;
|
||||
} | null;
|
||||
}
|
||||
|
||||
export interface ScrimPostRequest {
|
||||
|
|
@ -38,7 +45,7 @@ export interface ScrimPostRequest {
|
|||
createdAt: number;
|
||||
}
|
||||
|
||||
interface ScrimPostUser extends CommonUser {
|
||||
export interface ScrimPostUser extends CommonUser {
|
||||
isOwner: boolean;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,6 +30,10 @@
|
|||
text-align: center;
|
||||
}
|
||||
|
||||
.text-left {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.text-main-forced {
|
||||
color: var(--text) !important;
|
||||
}
|
||||
|
|
|
|||
24
docs/dev/templates/action.md
Normal file
24
docs/dev/templates/action.md
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
```ts
|
||||
// some-feature/actions/route.server.ts
|
||||
import type { ActionFunctionArgs } from "@remix-run/node";
|
||||
import { requireUserId } from "~/features/auth/core/user.server";
|
||||
import { parseRequestPayload } from "~/utils/remix.server";
|
||||
|
||||
export const action = async ({ request }: ActionFunctionArgs) => {
|
||||
const user = await requireUserId(request);
|
||||
const data = await parseRequestPayload({
|
||||
request,
|
||||
schema: actionSchema,
|
||||
});
|
||||
|
||||
// check permissions via requirePermission
|
||||
|
||||
// update via Repository
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// some-feature/routes/route.ts
|
||||
import { action } from "../actions/route.server.ts"
|
||||
export { action }
|
||||
```
|
||||
|
|
@ -92,4 +92,31 @@ test.describe("Scrims", () => {
|
|||
|
||||
await expect(page.getByText("Scheduled scrim")).toBeVisible();
|
||||
});
|
||||
|
||||
test("cancels a scrim and shows canceled status", async ({ page }) => {
|
||||
await seed(page);
|
||||
await impersonate(page, ADMIN_ID);
|
||||
await navigate({
|
||||
page,
|
||||
url: scrimsPage(),
|
||||
});
|
||||
|
||||
// Accept the first available scrim request to make it possible to access the scrim details page
|
||||
await page.getByRole("button", { name: "Accept" }).first().click();
|
||||
await page.getByTestId("confirm-button").click();
|
||||
|
||||
await page.getByRole("link", { name: "Contact" }).click();
|
||||
|
||||
// Cancel the scrim
|
||||
await page.getByRole("button", { name: "Cancel" }).click();
|
||||
await page.getByLabel("Reason").fill("Oops something came up");
|
||||
await page.getByTestId("cancel-scrim-submit").click();
|
||||
|
||||
// Go back to the scrims page and check if the scrim is marked as canceled
|
||||
await navigate({
|
||||
page,
|
||||
url: scrimsPage(),
|
||||
});
|
||||
await expect(page.getByText("Canceled")).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "Kopiér til udklipsholder",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Skab",
|
||||
"actions.close": "Luk",
|
||||
"actions.cancel": "Annuller",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "Scrim post löschen?",
|
||||
"deleteModal.prevented": "Der Post muss von Ersteller ({{username}}) gelöscht werden.",
|
||||
"cancelModal.title": "Anfrage zurückziehen?",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "Kontaktieren",
|
||||
"acceptModal.title": "Scrim-Anfrage von {{groupName}} akzeptieren & andere ablehnen (falls vorhanden)?",
|
||||
"acceptModal.prevented": "Bitte den Ersteller des Scrim-Posts, diese Anfrage anzunehmen.",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "Link zurücksetzen",
|
||||
"associations.removeMember.title": "{{username}} aus der Assoziation entfernen?",
|
||||
"associations.forms.title": "Neue Assoziation erstellen",
|
||||
"associations.forms.name.title": "Name"
|
||||
"associations.forms.name.title": "Name",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,10 +13,14 @@
|
|||
"pickup": "{{username}}'s pickup",
|
||||
"status.booked": "Booked",
|
||||
"status.pending": "Pending",
|
||||
"status.canceled": "Canceled",
|
||||
"actions.request": "Request",
|
||||
"deleteModal.title": "Delete the scrim post?",
|
||||
"deleteModal.prevented": "The post must be deleted by the owner ({{username}})",
|
||||
"cancelModal.title": "Cancel the request?",
|
||||
"cancelModal.scrim.title": "Cancel the scrim?",
|
||||
"cancelModal.scrim.reasonLabel": "Reason for cancellation",
|
||||
"cancelModal.scrim.reasonExplanation": "Explain why you are cancelling the scrim. This will be visible to the other team.",
|
||||
"actions.contact": "Contact",
|
||||
"acceptModal.title": "Accept the request to scrim by {{groupName}} & reject others (if any)?",
|
||||
"acceptModal.prevented": "Ask the person who posted the scrim to accept this request",
|
||||
|
|
@ -47,5 +51,8 @@
|
|||
"associations.shareLink.reset": "Reset link",
|
||||
"associations.removeMember.title": "Remove {{username}} from the association?",
|
||||
"associations.forms.title": "Creating a new association",
|
||||
"associations.forms.name.title": "Name"
|
||||
"associations.forms.name.title": "Name",
|
||||
"forms.managedByAnyone.title": "Anyone can manage",
|
||||
"forms.managedByAnyone.explanation": "If enabled, all users in this post can accept requests and delete the post, not just the owner.",
|
||||
"alert.canceled": "This scrim was canceled by {{user}}. Reason: {{reason}}"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "Copiar a clipboard",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Crear",
|
||||
"actions.close": "Cerrar",
|
||||
"actions.cancel": "Cancelar",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "Mostrar más",
|
||||
"actions.showLess": "Mostrar menos",
|
||||
"actions.copyToClipboard": "Copiar a clipboard",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Crear",
|
||||
"actions.close": "Cerrar",
|
||||
"actions.cancel": "Cancelar",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "Copier dans le presse-papier",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Créer",
|
||||
"actions.close": "Fermer",
|
||||
"actions.cancel": "Annuler",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "Montrer more",
|
||||
"actions.showLess": "Montrer moins",
|
||||
"actions.copyToClipboard": "Copier dans le presse-papier",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Créer",
|
||||
"actions.close": "Fermer",
|
||||
"actions.cancel": "Annuler le match",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "Supprimer le post?",
|
||||
"deleteModal.prevented": "Le post doit être supprimée par le propriétaire ({{username}})",
|
||||
"cancelModal.title": "Annuler la demande ?",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "Contact",
|
||||
"acceptModal.title": "Accepter la demande de scrim de {{groupName}} et rejeter les autres (s'il y en a)?",
|
||||
"acceptModal.prevented": "Demandez à la personne qui a posté le scrim d'accepter cette demande",
|
||||
|
|
@ -36,7 +39,6 @@
|
|||
"forms.divs.minDiv.title": "Min div",
|
||||
"forms.divs.maxDiv.title": "Max div",
|
||||
"page.scheduledScrim": "Scrim programmé",
|
||||
|
||||
"associations.title": "Association",
|
||||
"associations.explanation": "Créez une ''association'' pour regarder dans un groupe plus petit (par exemple, créez une association avec les adversaires habituels de votre équipe ou avec la division LUTI).",
|
||||
"associations.join.title": "Rejoindre l'association {{name}} ?",
|
||||
|
|
@ -48,5 +50,8 @@
|
|||
"associations.shareLink.reset": "Réinitialiser le lien",
|
||||
"associations.removeMember.title": "Enlever {{username}} de l'association?",
|
||||
"associations.forms.title": "Créer une nouvelle association",
|
||||
"associations.forms.name.title": "Nom"
|
||||
"associations.forms.name.title": "Nom",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "ההעתקה ללוח",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "יצירה",
|
||||
"actions.close": "סגירה",
|
||||
"actions.cancel": "ביטול",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "Mostra di più",
|
||||
"actions.showLess": "Mostra di meno",
|
||||
"actions.copyToClipboard": "Copia",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Crea",
|
||||
"actions.close": "Chiudi",
|
||||
"actions.cancel": "Cancella",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "もっと見せる",
|
||||
"actions.showLess": "表示を減らす",
|
||||
"actions.copyToClipboard": "クリップボードにコピーする",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "作成",
|
||||
"actions.close": "閉じる",
|
||||
"actions.cancel": "キャンセル",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "클립보드로 복사",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "생성",
|
||||
"actions.close": "닫기",
|
||||
"actions.cancel": "취소",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "Kopiëren naar klembord",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "",
|
||||
"actions.close": "",
|
||||
"actions.cancel": "",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "",
|
||||
"actions.showLess": "",
|
||||
"actions.copyToClipboard": "Skopiuj do schowka",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Stwórz",
|
||||
"actions.close": "Zamknij",
|
||||
"actions.cancel": "Anuluj",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "Mostrar mais",
|
||||
"actions.showLess": "Mostrar menos",
|
||||
"actions.copyToClipboard": "Copiar para a área de transferência",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Criar",
|
||||
"actions.close": "Fechar",
|
||||
"actions.cancel": "Cancelar",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "Показать больше",
|
||||
"actions.showLess": "Показать меньше",
|
||||
"actions.copyToClipboard": "Скопировать в буфер обмена",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "Создать",
|
||||
"actions.close": "Закрыть",
|
||||
"actions.cancel": "Отменить",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
|
|
@ -99,6 +99,7 @@
|
|||
"actions.showMore": "显示更多",
|
||||
"actions.showLess": "显示更少",
|
||||
"actions.copyToClipboard": "复制到剪贴板",
|
||||
"actions.copyTimestampForDiscord": "",
|
||||
"actions.create": "创建",
|
||||
"actions.close": "关闭",
|
||||
"actions.cancel": "取消",
|
||||
|
|
|
|||
|
|
@ -17,6 +17,9 @@
|
|||
"deleteModal.title": "",
|
||||
"deleteModal.prevented": "",
|
||||
"cancelModal.title": "",
|
||||
"cancelModal.scrim.title": "",
|
||||
"cancelModal.scrim.reasonLabel": "",
|
||||
"cancelModal.scrim.reasonExplanation": "",
|
||||
"actions.contact": "",
|
||||
"acceptModal.title": "",
|
||||
"acceptModal.prevented": "",
|
||||
|
|
@ -47,5 +50,8 @@
|
|||
"associations.shareLink.reset": "",
|
||||
"associations.removeMember.title": "",
|
||||
"associations.forms.title": "",
|
||||
"associations.forms.name.title": ""
|
||||
"associations.forms.name.title": "",
|
||||
"forms.managedByAnyone.title": "",
|
||||
"forms.managedByAnyone.explanation": "",
|
||||
"alert.canceled": ""
|
||||
}
|
||||
|
|
|
|||
16
migrations/088-scrimpost-managed-by-anyone.js
Normal file
16
migrations/088-scrimpost-managed-by-anyone.js
Normal file
|
|
@ -0,0 +1,16 @@
|
|||
export function up(db) {
|
||||
db.transaction(() => {
|
||||
db.prepare(
|
||||
/* sql */ `alter table "ScrimPost" add column "managedByAnyone" integer default 0 not null`,
|
||||
).run();
|
||||
db.prepare(
|
||||
/* sql */ `alter table "ScrimPost" add column "canceledAt" integer`,
|
||||
).run();
|
||||
db.prepare(
|
||||
/* sql */ `alter table "ScrimPost" add column "canceledByUserId" integer references "User"("id") on delete restrict`,
|
||||
).run();
|
||||
db.prepare(
|
||||
/* sql */ `alter table "ScrimPost" add column "cancelReason" text`,
|
||||
).run();
|
||||
})();
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user