mirror of
https://github.com/Sendouc/sendou.ink.git
synced 2026-04-24 06:58:10 -05:00
Edit managers in DB
This commit is contained in:
parent
bb96f631fe
commit
2a37365bd6
|
|
@ -1,6 +1,88 @@
|
|||
import { sql } from "../sql";
|
||||
import type { Badge, User } from "../types";
|
||||
|
||||
const deleteManyManagersStm = sql.prepare(`
|
||||
DELETE FROM
|
||||
"BadgeManager"
|
||||
WHERE
|
||||
"badgeId" = $badgeId
|
||||
`);
|
||||
|
||||
const createManagerStm = sql.prepare(`
|
||||
INSERT INTO
|
||||
"BadgeManager" (
|
||||
"badgeId",
|
||||
"userId"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
$badgeId,
|
||||
$userId
|
||||
)
|
||||
`);
|
||||
|
||||
export const upsertManyManagers = sql.transaction(
|
||||
({
|
||||
badgeId,
|
||||
managerIds,
|
||||
}: {
|
||||
badgeId: Badge["id"];
|
||||
managerIds: Array<User["id"]>;
|
||||
}) => {
|
||||
deleteManyManagersStm.run({
|
||||
badgeId,
|
||||
});
|
||||
|
||||
for (const userId of managerIds) {
|
||||
createManagerStm.run({
|
||||
userId,
|
||||
badgeId,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const deleteManyOwnersStm = sql.prepare(`
|
||||
DELETE FROM
|
||||
"BadgeOwner"
|
||||
WHERE
|
||||
"badgeId" = $badgeId
|
||||
`);
|
||||
|
||||
const createOwnerStm = sql.prepare(`
|
||||
INSERT INTO
|
||||
"BadgeOwner" (
|
||||
"badgeId",
|
||||
"userId"
|
||||
)
|
||||
VALUES
|
||||
(
|
||||
$badgeId,
|
||||
$userId
|
||||
)
|
||||
`);
|
||||
|
||||
export const upsertManyOwners = sql.transaction(
|
||||
({
|
||||
badgeId,
|
||||
ownerIds,
|
||||
}: {
|
||||
badgeId: Badge["id"];
|
||||
ownerIds: Array<User["id"]>;
|
||||
}) => {
|
||||
deleteManyOwnersStm.run({
|
||||
badgeId,
|
||||
});
|
||||
|
||||
for (const userId of ownerIds) {
|
||||
createOwnerStm.run({
|
||||
userId,
|
||||
badgeId,
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
const countsByUserIdStm = sql.prepare(`
|
||||
select
|
||||
"Badge"."code",
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import * as React from "react";
|
||||
import { useMatches, useOutletContext } from "@remix-run/react";
|
||||
import { Form, useMatches, useOutletContext } from "@remix-run/react";
|
||||
import { Button, LinkButton } from "~/components/Button";
|
||||
import { Dialog } from "~/components/Dialog";
|
||||
import { atOrError } from "~/utils/arrays";
|
||||
|
|
@ -7,30 +7,79 @@ import type { BadgeDetailsContext, BadgeDetailsLoaderData } from "../$id";
|
|||
import { discordFullName } from "~/utils/strings";
|
||||
import { UserCombobox } from "~/components/Combobox";
|
||||
import { TrashIcon } from "~/components/icons/Trash";
|
||||
import { z } from "zod";
|
||||
import { actualNumber, noDuplicates, safeJSONParse, id } from "~/utils/zod";
|
||||
import type { ActionFunction } from "@remix-run/node";
|
||||
import { requireUser, useUser } from "~/modules/auth";
|
||||
import { parseRequestFormData, validate } from "~/utils/remix";
|
||||
import { canEditBadgeManagers } from "~/permissions";
|
||||
import { assertUnreachable } from "~/utils/types";
|
||||
import { db } from "~/db";
|
||||
|
||||
const editBadgeActionSchema = z.union([
|
||||
z.object({
|
||||
_action: z.literal("MANAGERS"),
|
||||
managerIds: z.preprocess(safeJSONParse, z.array(id).refine(noDuplicates)),
|
||||
}),
|
||||
z.object({
|
||||
_action: z.literal("OWNERS"),
|
||||
ownerIds: z.preprocess(safeJSONParse, z.array(id).refine(noDuplicates)),
|
||||
}),
|
||||
]);
|
||||
|
||||
export const action: ActionFunction = async ({ request, params }) => {
|
||||
const data = await parseRequestFormData({
|
||||
request,
|
||||
schema: editBadgeActionSchema,
|
||||
});
|
||||
const badgeId = z.preprocess(actualNumber, z.number()).parse(params["id"]);
|
||||
const user = await requireUser(request);
|
||||
|
||||
validate(canEditBadgeManagers(user));
|
||||
|
||||
switch (data._action) {
|
||||
case "MANAGERS": {
|
||||
db.badges.upsertManyManagers({ badgeId, managerIds: data.managerIds });
|
||||
break;
|
||||
}
|
||||
case "OWNERS": {
|
||||
db.badges.upsertManyOwners({ badgeId, ownerIds: data.ownerIds });
|
||||
break;
|
||||
}
|
||||
default: {
|
||||
assertUnreachable(data);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
// xxx: on SSR modal flickers first shown at top
|
||||
export default function EditBadgePage() {
|
||||
const user = useUser();
|
||||
const matches = useMatches();
|
||||
const data = atOrError(matches, -2).data as BadgeDetailsLoaderData;
|
||||
const { badgeName } = useOutletContext<BadgeDetailsContext>();
|
||||
|
||||
return (
|
||||
<Dialog isOpen className="stack md">
|
||||
<div>
|
||||
<h2 className="badges-edit__big-header">
|
||||
Editing winners of {badgeName}
|
||||
</h2>
|
||||
<LinkButton
|
||||
to={atOrError(matches, -2).pathname}
|
||||
variant="minimal-destructive"
|
||||
tiny
|
||||
>
|
||||
Cancel
|
||||
</LinkButton>
|
||||
</div>
|
||||
<Form method="post">
|
||||
<div>
|
||||
<h2 className="badges-edit__big-header">
|
||||
Editing winners of {badgeName}
|
||||
</h2>
|
||||
<LinkButton
|
||||
to={atOrError(matches, -2).pathname}
|
||||
variant="minimal-destructive"
|
||||
tiny
|
||||
>
|
||||
Cancel
|
||||
</LinkButton>
|
||||
</div>
|
||||
|
||||
<Managers data={data} />
|
||||
<Owners data={data} />
|
||||
{canEditBadgeManagers(user) ? <Managers data={data} /> : null}
|
||||
<Owners data={data} />
|
||||
</Form>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
|
@ -87,13 +136,20 @@ function Managers({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
userIdsToOmit={new Set(managers.map((m) => m.id))}
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
name="managerIds"
|
||||
value={JSON.stringify(managers.map((m) => m.id))}
|
||||
/>
|
||||
<Button
|
||||
type="submit"
|
||||
tiny
|
||||
className="w-full"
|
||||
className="badges-edit__submit-button"
|
||||
disabled={amountOfChanges === 0}
|
||||
name="_action"
|
||||
value="MANAGERS"
|
||||
>
|
||||
{amountOfChanges > 0 ? `Submit ${amountOfChanges} changes` : "Submit"}
|
||||
{submitButtonText(amountOfChanges)}
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -111,3 +167,10 @@ function Owners({ data }: { data: BadgeDetailsLoaderData }) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function submitButtonText(amountOfChanges: number) {
|
||||
if (amountOfChanges === 0) return "Submit";
|
||||
if (amountOfChanges === 1) return `Submit ${amountOfChanges} change`;
|
||||
|
||||
return `Submit ${amountOfChanges} changes`;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -76,3 +76,7 @@
|
|||
.badges-edit__small-header {
|
||||
font-size: var(--fonts-md);
|
||||
}
|
||||
|
||||
.badges-edit__submit-button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
import { z } from "zod";
|
||||
|
||||
export const id = z.number().int().positive();
|
||||
|
||||
export function safeJSONParse(value: unknown): unknown {
|
||||
try {
|
||||
if (typeof value !== "string") return value;
|
||||
|
|
@ -21,3 +23,7 @@ export function actualNumber(value: unknown) {
|
|||
|
||||
return Number.isNaN(parsed) ? undefined : parsed;
|
||||
}
|
||||
|
||||
export function noDuplicates(arr: (number | string)[]) {
|
||||
return new Set(arr).size === arr.length;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user