Small changes to label form

This commit is contained in:
Ryan Pandya 2024-11-18 10:04:25 -08:00
parent b103b5418b
commit 0802e36796
8 changed files with 173 additions and 150 deletions

View File

@ -0,0 +1,3 @@
2024-11-18T16:54:25.722Z error: Authentication error. User: "ryan@pandu.ski", Message: "no such table: user", IP-Address: "::ffff:127.0.0.1"
2024-11-18T16:54:31.817Z error: Authentication error. User: "ryan@ryanpandya.com", Message: "no such table: user", IP-Address: "::ffff:127.0.0.1"
2024-11-18T16:54:43.805Z error: Authentication error. User: "ryan@ryanpandya.com", Message: "no such table: user", IP-Address: "::ffff:127.0.0.1"

View File

@ -34,6 +34,7 @@ import { useForm } from "react-hook-form";
import { z } from "zod"; import { z } from "zod";
import { zLabelSchema } from "@lifetracker/shared/types/labels"; import { zLabelSchema } from "@lifetracker/shared/types/labels";
import { Textarea } from "@/components/ui/textarea";
type CreateLabelSchema = z.infer<typeof zLabelSchema>; type CreateLabelSchema = z.infer<typeof zLabelSchema>;
@ -46,13 +47,6 @@ export default function AddLabelDialog({
const [isOpen, onOpenChange] = useState(false); const [isOpen, onOpenChange] = useState(false);
const form = useForm<CreateLabelSchema>({ const form = useForm<CreateLabelSchema>({
resolver: zodResolver(zLabelSchema), resolver: zodResolver(zLabelSchema),
defaultValues: {
id: "69",
name: "Fuckdicks",
code: 420,
description: "This shit sucks",
color: "#004400",
},
}); });
const { mutate, isPending } = api.labels.createLabel.useMutation({ const { mutate, isPending } = api.labels.createLabel.useMutation({
onSuccess: () => { onSuccess: () => {
@ -87,56 +81,75 @@ export default function AddLabelDialog({
<DialogTrigger asChild>{children}</DialogTrigger> <DialogTrigger asChild>{children}</DialogTrigger>
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle>Add User</DialogTitle> <DialogTitle>Create Label</DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form onSubmit={form.handleSubmit((val) => mutate(val))}> <form onSubmit={form.handleSubmit((val) => mutate(val))}>
<div className="flex w-full flex-col space-y-2"> <div className="flex w-full flex-col space-y-2">
<FormField <div style={{ display: "grid", gridTemplateColumns: "5em 1fr 75px", gap: "10px" }}>
control={form.control} <FormField
name="name" control={form.control}
render={({ field }) => ( name="code"
<FormItem> render={({ field }) => (
<FormLabel>Name</FormLabel> <FormItem>
<FormControl> {/* <FormLabel>Code</FormLabel> */}
<Input <FormControl>
type="text" <Input
placeholder="Name" type="text"
{...field} placeholder="Code"
className="w-full rounded border p-2" {...field}
/> className="w-full rounded border p-2"
</FormControl> />
<FormMessage /> </FormControl>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
<FormField />
control={form.control} <FormField
name="code" control={form.control}
render={({ field }) => ( name="name"
<FormItem> render={({ field }) => (
<FormLabel>Code</FormLabel> <FormItem>
<FormControl> {/* <FormLabel>Name</FormLabel> */}
<Input <FormControl>
type="number" <Input
placeholder="Code" type="text"
{...field} placeholder="Name"
className="w-full rounded border p-2" {...field}
/> className="w-full rounded border p-2"
</FormControl> />
<FormMessage /> </FormControl>
</FormItem> <FormMessage />
)} </FormItem>
/> )}
/>
<FormField
control={form.control}
name="color"
render={({ field }) => (
<FormItem>
{/* <FormLabel>Color</FormLabel> */}
<FormControl>
<Input
type="text"
placeholder="Color"
{...field}
className="w-full rounded border p-2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<FormField <FormField
control={form.control} control={form.control}
name="description" name="description"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>Description</FormLabel> {/* <FormLabel>Description</FormLabel> */}
<FormControl> <FormControl>
<Input <Textarea
type="text"
placeholder="Description" placeholder="Description"
{...field} {...field}
className="w-full rounded border p-2" className="w-full rounded border p-2"
@ -146,24 +159,6 @@ export default function AddLabelDialog({
</FormItem> </FormItem>
)} )}
/> />
<FormField
control={form.control}
name="color"
render={({ field }) => (
<FormItem>
<FormLabel>Color, hope you like hex codes</FormLabel>
<FormControl>
<Input
type="text"
placeholder="Color"
{...field}
className="w-full rounded border p-2"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter className="sm:justify-end"> <DialogFooter className="sm:justify-end">
<DialogClose asChild> <DialogClose asChild>
<Button type="button" variant="secondary"> <Button type="button" variant="secondary">

View File

@ -3,26 +3,26 @@ import { ActionButton } from "@/components/ui/action-button";
import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog"; import ActionConfirmingDialog from "@/components/ui/action-confirming-dialog";
import { toast } from "@/components/ui/use-toast"; import { toast } from "@/components/ui/use-toast";
import { useDeleteTag } from "@hoarder/shared-react/hooks/tags"; import { useDeleteLabel } from "@lifetracker/shared-react/hooks/labels";
export default function DeleteTagConfirmationDialog({ export default function DeleteLabelConfirmationDialog({
tag, label,
open, open,
setOpen, setOpen,
}: { }: {
tag: { id: string; name: string }; label: { id: string; code: number; name: string };
open?: boolean; open?: boolean;
setOpen?: (v: boolean) => void; setOpen?: (v: boolean) => void;
}) { }) {
const currentPath = usePathname(); const currentPath = usePathname();
const router = useRouter(); const router = useRouter();
const { mutate: deleteTag, isPending } = useDeleteTag({ const { mutate: deleteTag, isPending } = useDeleteLabel({
onSuccess: () => { onSuccess: () => {
toast({ toast({
description: `Tag "${tag.name}" has been deleted!`, description: `Label "${label.name}" has been deleted!`,
}); });
if (currentPath.includes(tag.id)) { if (currentPath.includes(label.code)) {
router.push("/dashboard/tags"); router.push("/dashboard/labels");
} }
}, },
onError: () => { onError: () => {
@ -36,8 +36,8 @@ export default function DeleteTagConfirmationDialog({
<ActionConfirmingDialog <ActionConfirmingDialog
open={open} open={open}
setOpen={setOpen} setOpen={setOpen}
title={`Delete ${tag.name}?`} title={`Delete ${label.name}?`}
description={`Are you sure you want to delete the tag "${tag.name}"?`} description={`Are you sure you want to delete the label "${label.name}"?`}
actionButton={(setDialogOpen) => ( actionButton={(setDialogOpen) => (
<ActionButton <ActionButton
type="button" type="button"
@ -45,7 +45,7 @@ export default function DeleteTagConfirmationDialog({
loading={isPending} loading={isPending}
onClick={() => onClick={() =>
deleteTag( deleteTag(
{ tagId: tag.id }, { labelId: label.id },
{ onSuccess: () => setDialogOpen(false) }, { onSuccess: () => setDialogOpen(false) },
) )
} }

View File

@ -17,12 +17,30 @@ import { Check, KeyRound, Pencil, Trash, FilePlus, X } from "lucide-react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import AddLabelDialog from "./AddLabelDialog"; import AddLabelDialog from "./AddLabelDialog";
import EditLabelDialog from "./EditLabelDialog"; import EditLabelDialog from "./EditLabelDialog";
import DeleteLabelConfirmationDialog from "./DeleteLabelConfirmationDialog";
export default function LabelsView() { export default function LabelsView() {
const { data: session } = useSession(); const { data: session } = useSession();
const { data: labels } = api.labels.list.useQuery(); const { data: labels } = api.labels.list.useQuery();
const { data: labelStats } = api.labels.labelStats.useQuery(); const { data: labelStats } = api.labels.labelStats.useQuery();
const invalidateLabelList = api.useUtils().labels.list.invalidate;
const { mutate: deleteLabel, isPending: isDeletionPending } =
api.labels.delete.useMutation({
onSuccess: () => {
toast({
description: "Label deleted",
});
invalidateLabelList();
},
onError: (e) => {
toast({
variant: "destructive",
description: `Something went wrong: ${e.message}`,
});
},
});
const LabelsTable = ({ labels }) => ( const LabelsTable = ({ labels }) => (
<Table> <Table>
<TableHeader className="bg-gray-200"> <TableHeader className="bg-gray-200">

View File

@ -7,17 +7,19 @@ import path from "path";
import dbConfig from "./drizzle.config"; import dbConfig from "./drizzle.config";
console.log("dbURL: ", dbConfig.dbCredentials.url);
const sqlite = new Database(dbConfig.dbCredentials.url); const sqlite = new Database(dbConfig.dbCredentials.url);
export const db = drizzle(sqlite, { export const db = drizzle(sqlite, {
schema, schema,
// logger: true logger: true
}); });
export function getInMemoryDB(runMigrations: boolean) { export function getInMemoryDB(runMigrations: boolean) {
const mem = new Database(":memory:"); const mem = new Database(":memory:");
const db = drizzle(mem, { schema, logger: true }); const db = drizzle(mem, { schema, logger: true });
if (runMigrations) { if (runMigrations) {
migrate(db, { migrationsFolder: path.resolve(__dirname, "./drizzle") }); migrate(db, { migrationsFolder: path.resolve(__dirname, "./migrations") });
} }
return db; return db;
} }

View File

@ -0,0 +1,50 @@
import { api } from "../trpc";
export function useUpdateLabel(
...opts: Parameters<typeof api.labels.update.useMutation>
) {
const apiUtils = api.useUtils();
return api.tags.update.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.labels.list.invalidate();
apiUtils.labels.get.invalidate({ labelId: res.id });
// apiUtils.bookmarks.getBookmarks.invalidate({
// labelId: res.id;
// TODO: Maybe we can only look at the cache and invalidate only affected bookmarks
// apiUtils.bookmarks.getBookmark.invalidate();
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}
export function useDeleteLabel(
...opts: Parameters<typeof api.labels.delete.useMutation>
) {
const apiUtils = api.useUtils();
return api.labels.delete.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.labels.list.invalidate();
// apiUtils.bookmarks.getBookmark.invalidate();
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}
export function useDeleteUnusedTags(
...opts: Parameters<typeof api.tags.deleteUnused.useMutation>
) {
const apiUtils = api.useUtils();
return api.tags.deleteUnused.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.tags.list.invalidate();
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}

View File

@ -1,69 +0,0 @@
import { api } from "../trpc";
export function useUpdateTag(
...opts: Parameters<typeof api.tags.update.useMutation>
) {
const apiUtils = api.useUtils();
return api.tags.update.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.tags.list.invalidate();
apiUtils.tags.get.invalidate({ tagId: res.id });
apiUtils.bookmarks.getBookmarks.invalidate({ tagId: res.id });
// TODO: Maybe we can only look at the cache and invalidate only affected bookmarks
apiUtils.bookmarks.getBookmark.invalidate();
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}
export function useMergeTag(
...opts: Parameters<typeof api.tags.merge.useMutation>
) {
const apiUtils = api.useUtils();
return api.tags.merge.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.tags.list.invalidate();
[res.mergedIntoTagId, ...res.deletedTags].forEach((tagId) => {
apiUtils.tags.get.invalidate({ tagId });
apiUtils.bookmarks.getBookmarks.invalidate({ tagId });
});
// TODO: Maybe we can only look at the cache and invalidate only affected bookmarks
apiUtils.bookmarks.getBookmark.invalidate();
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}
export function useDeleteTag(
...opts: Parameters<typeof api.tags.delete.useMutation>
) {
const apiUtils = api.useUtils();
return api.tags.delete.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.tags.list.invalidate();
apiUtils.bookmarks.getBookmark.invalidate();
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}
export function useDeleteUnusedTags(
...opts: Parameters<typeof api.tags.deleteUnused.useMutation>
) {
const apiUtils = api.useUtils();
return api.tags.deleteUnused.useMutation({
...opts[0],
onSuccess: (res, req, meta) => {
apiUtils.tags.list.invalidate();
return opts[0]?.onSuccess?.(res, req, meta);
},
});
}

View File

@ -185,9 +185,6 @@ export const labelsAppRouter = router({
}), }),
) )
.mutation(async ({ input, ctx }) => { .mutation(async ({ input, ctx }) => {
console.log(input);
console.log(ctx);
console.log("TEEEEEEEEEEEEEEEEEEEEEST\n\n\n\n\n");
try { try {
const res = await ctx.db const res = await ctx.db
.update(bookmarkTags) .update(bookmarkTags)
@ -238,4 +235,31 @@ export const labelsAppRouter = router({
throw e; throw e;
} }
}), }),
delete: authedProcedure
.input(
z.object({
labelId: z.string(),
}),
)
.mutation(async ({ input, ctx }) => {
// const affectedBookmarks = await ctx.db
// .select({
// bookmarkId: tagsOnBookmarks.bookmarkId,
// })
// .from(tagsOnBookmarks)
// .where(
// and(
// eq(tagsOnBookmarks.tagId, input.tagId),
// // Tag ownership is checked in the ensureTagOwnership middleware
// // eq(bookmarkTags.userId, ctx.user.id),
// ),
// );
const res = await ctx.db
.delete(labels)
.where(conditionFromInput(input, ctx.user.id));
if (res.changes == 0) {
throw new TRPCError({ code: "NOT_FOUND" });
}
}),
}); });