Edit managers in DB

This commit is contained in:
Kalle 2022-07-07 12:30:08 +03:00
parent bb96f631fe
commit 2a37365bd6
4 changed files with 172 additions and 17 deletions

View File

@ -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",

View File

@ -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`;
}

View File

@ -76,3 +76,7 @@
.badges-edit__small-header {
font-size: var(--fonts-md);
}
.badges-edit__submit-button {
margin: 0 auto;
}

View File

@ -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;
}