implement 'edit' functionality for pubs

This commit is contained in:
andrzej 2024-09-27 12:17:37 +02:00
parent 9bc1750848
commit 5b4e961859
15 changed files with 117 additions and 83 deletions

Binary file not shown.

View File

@ -6,6 +6,8 @@ import { revalidatePath } from "next/cache"
import { redirect } from "next/navigation"
import { storySchema, subSchema } from "app/ui/forms/schemas"
import { z } from "zod"
import { StoryWithGenres } from "app/story/page"
import { PubWithGenres } from "app/publication/page"
export async function updateField({ datum, table, column, id, pathname }: { datum?: string | number | Genre[], table: string, column: string, id: number, pathname: string }) {
@ -22,7 +24,7 @@ export async function updateField({ datum, table, column, id, pathname }: { datu
return res
} catch (error) {
console.error(error)
return false
return null
}
}
@ -40,11 +42,11 @@ export async function updateGenres({ genres, table, id, pathname }: { genres: {
return res
} catch (error) {
console.error(error)
return false
return null
}
}
export async function updateSub(data: Sub): Promise<Sub | boolean> {
export async function updateSub(data: Sub): Promise<Sub> {
"use server"
try {
subSchema.parse(data)
@ -53,12 +55,12 @@ export async function updateSub(data: Sub): Promise<Sub | boolean> {
return res
} catch (error) {
console.error(error)
return false
return null
}
}
export async function updateStory(data: Story & { genres: number[] }): Promise<Story | boolean> {
export async function updateStory(data: StoryWithGenres): Promise<{ success: string }> {
"use server"
try {
@ -66,25 +68,25 @@ export async function updateStory(data: Story & { genres: number[] }): Promise<S
const storyData = await prepStoryData(data)
const genresArray = await prepGenreData(data.genres)
//submit
const res = prisma.story.update({
const res = await prisma.story.update({
where: { id: data.id },
data: storyData
})
await prisma.story.update({
const genreRes = await prisma.story.update({
where: { id: data.id },
data: {
genres: { set: genresArray }
}
})
return res
return { success: "Updated the story '" + res.title + "'." }
} catch (error) {
console.error(error)
return false
return null
}
}
export async function updatePub(data: Pub & { genres: number[] }): Promise<Pub | boolean> {
export async function updatePub(data: PubWithGenres): Promise<{ success: string }> {
"use server"
try {
@ -93,7 +95,7 @@ export async function updatePub(data: Pub & { genres: number[] }): Promise<Pub |
(data)
const genresArray = await prepGenreData(data.genres)
//submit
const res = prisma.pub.update({
const res = await prisma.pub.update({
where: { id: data.id },
data: pubData
})
@ -103,10 +105,10 @@ export async function updatePub(data: Pub & { genres: number[] }): Promise<Pub |
genres: { set: genresArray }
}
})
return res
return { success: "Updated the publication '" + res.title + "'" }
} catch (error) {
console.error(error)
return false
return null
}
}

View File

@ -2,6 +2,7 @@ import { z } from "zod";
import { storySchema } from "app/ui/forms/schemas";
import { Pub, Story } from "@prisma/client";
import { pubSchema } from "app/ui/forms/schemas";
import { StoryWithGenres } from "app/story/page";
//schemas
@ -10,7 +11,7 @@ const pubSchemaTrimmed = pubSchema.omit({ genres: true })
const genreSchema = z.object({ id: z.number() })
const genresSchema = z.array(genreSchema)
export async function prepStoryData(data: Story & { genres: number[] }): Promise<{ title: string, word_count: number }> {
export async function prepStoryData(data: StoryWithGenres): Promise<{ title: string, word_count: number }> {
const storyData = structuredClone(data)
delete storyData.genres
//throw an error if validation fails
@ -18,7 +19,7 @@ export async function prepStoryData(data: Story & { genres: number[] }): Promise
return storyData
}
export async function prepPubData(data: Pub & { genres: number[] }): Promise<Pub | Boolean> {
export async function prepPubData(data: Pub & { genres: number[] }): Promise<Pub> {
const pubData = structuredClone(data)
delete pubData.genres
pubSchemaTrimmed.safeParse(pubData)

View File

@ -2,7 +2,7 @@
import { ColumnDef, createColumnHelper } from "@tanstack/react-table"
import { ArrowUpDown, BookType, Clock, Drama, SquareArrowOutUpRight } from "lucide-react"
import { Button } from "@/components/ui/button"
import { PubsWithGenres } from "./page"
import { PubWithGenres } from "./page"
import { TextInputCell } from "app/ui/tables/inputs/textInput"
import { selectCol } from "app/ui/tables/selectColumn"
import NumberInputCell from "app/ui/tables/inputs/numberInput"
@ -10,9 +10,9 @@ import { pubSchema } from "app/ui/forms/schemas"
import GenrePickerInputCell from "app/ui/tables/inputs/genrePickerInput"
const columnHelper = createColumnHelper<PubsWithGenres>()
const columnHelper = createColumnHelper<PubWithGenres>()
export const columns: ColumnDef<PubsWithGenres>[] = [
export const columns: ColumnDef<PubWithGenres>[] = [
selectCol,
{
accessorKey: "title",
@ -26,7 +26,7 @@ export const columns: ColumnDef<PubsWithGenres>[] = [
Title
</span>
<span className="block sm:hidden"><BookType /></span>
<ArrowUpDown className="ml-2 h-4 w-4" />
<ArrowUpDown className="ml-2 h-4 w-4 hidden md:block" />
</Button>
)
},

View File

@ -7,34 +7,23 @@ import { createPub } from "app/lib/create";
import PubForm from "app/ui/forms/pub";
import { Plus } from "lucide-react";
import { useState } from "react";
import { PubWithGenres } from "./page";
export default function CreatePubDialog({ genres }: ComponentProps<"div"> & { genres: Genre[] }) {
export default function EditPubDialog({ genres, closeDialog, defaults, dbAction }: ComponentProps<"div"> & { genres: Genre[], closeDialog: () => void, defaults: PubWithGenres, dbAction: (data: PubWithGenres) => Promise<{ success: string }> }) {
const [isOpen, setIsOpen] = useState(false)
function closeDialog() {
setIsOpen(false)
}
return (
<Dialog open={isOpen} onOpenChange={setIsOpen}>
<DialogTrigger asChild>
<Button>
<span className="hidden md:block">Create new publication</span>
<Plus className="block md:hidden" />
</Button>
</DialogTrigger>
<DialogContent>
<DialogHeader>
<DialogTitle>Edit publication</DialogTitle>
<DialogDescription>Modify an entry for an existing publication.</DialogDescription>
</DialogHeader>
<PubForm dbAction={createPub} genres={genres} closeDialog={closeDialog} />
<DialogFooter>
<Button form="pubform">Submit</Button>
</DialogFooter>
<>
<DialogHeader>
<DialogTitle>Edit publication</DialogTitle>
<DialogDescription>Modify an entry for an existing publication.</DialogDescription>
</DialogHeader>
<PubForm dbAction={dbAction} genres={genres} closeDialog={closeDialog} defaults={defaults} />
<DialogFooter>
<Button form="pubform">Submit</Button>
</DialogFooter>
</>
</DialogContent>
</Dialog>
)
}

View File

@ -4,7 +4,7 @@ import { columns } from "./columns";
import { DataTable } from "app/ui/tables/data-table";
import CreatePubDialog from "./create";
export type PubsWithGenres = Pub & { genres: Array<Genre> }
export type PubWithGenres = Pub & { genres: Array<Genre> }

View File

@ -25,7 +25,7 @@ export const columns: ColumnDef<StoryWithGenres>[] = [
Title
</span>
<span className="block sm:hidden"><BookType /></span>
<ArrowUpDown className="ml-2 h-4 w-4" />
<ArrowUpDown className="ml-2 h-4 w-4 hidden md:block" />
</Button>
)
},
@ -52,7 +52,7 @@ export const columns: ColumnDef<StoryWithGenres>[] = [
<span className="sm:hidden">
<Tally5 />
</span>
<ArrowUpDown className="ml-2 h-4 w-4" />
<ArrowUpDown className="ml-2 h-4 w-4 hidden md:block" />
</Button>
)
},

View File

@ -1,13 +1,13 @@
"use client"
import { createStory } from "app/lib/create"
import { Dialog, DialogHeader, DialogTrigger, DialogContent, DialogClose, DialogTitle, DialogFooter, DialogDescription } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { ComponentProps, useState } from "react";
import { Genre, Story } from "@prisma/client";
import StoryForm from "app/ui/forms/story";
import { StoryWithGenres } from "./page";
export default function EditStoryDialog({ genres, closeDialog, defaults }: ComponentProps<"div"> & { genres: Genre[], closeDialog: () => void, defaults: Story }) {
export default function EditStoryDialog({ genres, closeDialog, defaults, dbAction }: ComponentProps<"div"> & { genres: Genre[], closeDialog: () => void, defaults: StoryWithGenres, dbAction: (data: StoryWithGenres) => Promise<{ success: string }> }) {
return (
@ -16,7 +16,7 @@ export default function EditStoryDialog({ genres, closeDialog, defaults }: Compo
<DialogTitle>Edit story</DialogTitle>
<DialogDescription>Create an entry for a new story i.e. a thing you intend to submit for publication.</DialogDescription>
</DialogHeader>
<StoryForm dbAction={createStory} genres={genres} className="" closeDialog={closeDialog} defaults={defaults} />
<StoryForm dbAction={dbAction} genres={genres} className="" closeDialog={closeDialog} defaults={defaults} />
<DialogFooter>
<Button form="storyform">Submit</Button>
</DialogFooter>

View File

@ -50,7 +50,7 @@ export const columns: ColumnDef<SubComplete>[] = [
return (
<Button
variant="ghost"
className="p-0"
className="p-0 flex justify-center w-full"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="hidden md:block"> Date Submitted </span>
@ -76,7 +76,7 @@ export const columns: ColumnDef<SubComplete>[] = [
return (
<Button
variant="ghost"
className="p-0"
className="p-0 flex justify-center w-full"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="hidden md:block"> Date Responded </span>
@ -105,7 +105,7 @@ export const columns: ColumnDef<SubComplete>[] = [
return (
<Button
variant="ghost"
className="p-0"
className="p-0 flex justify-center w-full"
onClick={() => column.toggleSorting(column.getIsSorted() === "asc")}
>
<span className="hidden md:block"> Response </span>

View File

@ -706,6 +706,10 @@ body {
z-index: 100;
}
.float-right {
float: right;
}
.m-auto {
margin: auto;
}
@ -777,14 +781,6 @@ body {
margin-top: 1.5rem;
}
.mt-auto {
margin-top: auto;
}
.mb-4 {
margin-bottom: 1rem;
}
.block {
display: block;
}
@ -887,10 +883,6 @@ body {
height: 100vh;
}
.h-5\/6 {
height: 83.333333%;
}
.max-h-96 {
max-height: 24rem;
}
@ -976,8 +968,8 @@ body {
width: 100vw;
}
.w-5\/6 {
width: 83.333333%;
.w-1\/2 {
width: 50%;
}
.min-w-\[8rem\] {
@ -1021,6 +1013,18 @@ body {
max-width: 20rem;
}
.max-w-40 {
max-width: 10rem;
}
.max-w-\[50\%\] {
max-width: 50%;
}
.max-w-60 {
max-width: 15rem;
}
.shrink-0 {
flex-shrink: 0;
}
@ -1105,6 +1109,18 @@ body {
flex-wrap: wrap;
}
.content-center {
align-content: center;
}
.content-start {
align-content: flex-start;
}
.content-between {
align-content: space-between;
}
.items-start {
align-items: flex-start;
}
@ -1129,6 +1145,10 @@ body {
justify-content: space-between;
}
.justify-around {
justify-content: space-around;
}
.gap-1 {
gap: 0.25rem;
}
@ -1220,6 +1240,10 @@ body {
margin-bottom: calc(2rem * var(--tw-space-y-reverse));
}
.self-end {
align-self: flex-end;
}
.justify-self-end {
justify-self: end;
}
@ -1267,11 +1291,6 @@ body {
border-top-right-radius: 1.5rem;
}
.rounded-l-3xl {
border-top-left-radius: 1.5rem;
border-bottom-left-radius: 1.5rem;
}
.rounded-tl-3xl {
border-top-left-radius: 1.5rem;
}
@ -1493,6 +1512,10 @@ body {
text-align: center;
}
.align-top {
vertical-align: top;
}
.align-middle {
vertical-align: middle;
}
@ -1536,6 +1559,11 @@ body {
line-height: 1rem;
}
.text-6xl {
font-size: 3.75rem;
line-height: 1;
}
.font-black {
font-weight: 900;
}

View File

@ -22,15 +22,17 @@ import GenrePicker from "./genrePicker"
import { pubSchema } from "./schemas"
import { useRouter } from "next/navigation"
import { Ban } from "lucide-react"
import { PubWithGenres } from "app/publication/page"
export default function PubForm({ genres, dbAction, className, closeDialog, defaults }: ComponentProps<"div"> & { genres: Array<Genre>, dbAction: (data: any) => Promise<{ success: string }>, closeDialog: () => void, defaults?: Pub }) {
export default function PubForm({ genres, dbAction, className, closeDialog, defaults }: ComponentProps<"div"> & { genres: Array<Genre>, dbAction: (data: any) => Promise<{ success: string }>, closeDialog: () => void, defaults?: PubWithGenres }) {
const form = useForm<z.infer<typeof pubSchema>>({
resolver: zodResolver(pubSchema),
defaultValues: {
title: "",
link: "",
query_after_days: 30,
genres: []
id: defaults?.id,
title: defaults?.title ?? "",
link: defaults?.link ?? "",
query_after_days: defaults?.query_after_days ?? 30,
genres: defaults?.genres.map(e => e.id) ?? []
},
})
@ -39,7 +41,7 @@ export default function PubForm({ genres, dbAction, className, closeDialog, defa
async function onSubmit(values: z.infer<typeof pubSchema>) {
try {
const res = await dbAction(values)
if (!res) throw new Error("something went wrong")
if (!res?.success) throw new Error("something went wrong")
toast({ title: "Success!", description: res.success })
router.refresh()
closeDialog()

View File

@ -7,6 +7,7 @@ export const storySchema = z.object({
})
export const pubSchema = z.object({
id: z.coerce.number().optional(),
title: z.string().min(2).max(50),
link: z.string(),
query_after_days: z.coerce.number().min(30),

View File

@ -34,6 +34,7 @@ export default function StoryForm({ genres, dbAction, className, closeDialog, de
const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
defaultValues: {
id: defaults?.id,
title: defaults?.title ?? "",
word_count: defaults?.word_count ?? 500,
genres: defaults?.genres.map(e => e.id) ?? []
@ -47,7 +48,7 @@ export default function StoryForm({ genres, dbAction, className, closeDialog, de
try {
const res = await dbAction(values)
//server actions return undefined if middleware authentication fails
if (!res.success) throw new Error("something went wrong")
if (!res?.success) throw new Error("something went wrong")
toast({ title: "Success!", description: res.success })
router.refresh()
closeDialog()
@ -86,7 +87,7 @@ export default function StoryForm({ genres, dbAction, className, closeDialog, de
)}
/>
<div className="inline-flex flex-wrap w-full gap-x-16 gap-y-8 items-baseline max-w-full">
<div className="inline-flex flex-wrap justify-around items-start w-full gap-x-16 gap-y-8 items-baseline max-w-full">
<GenrePicker
genres={genres}
@ -97,7 +98,7 @@ export default function StoryForm({ genres, dbAction, className, closeDialog, de
control={form.control}
name="word_count"
render={({ field }) => (
<FormItem className="flex flex-col">
<FormItem className="flex flex-col ">
<FormLabel className="h-5">Word count</FormLabel>
<FormControl>
<Input className=" w-24" type="number" step={500} {...field}></Input>

View File

@ -5,7 +5,7 @@ import { Badge } from "@/components/ui/badge";
export default function GenreBadges(props: ComponentProps<"div"> & { genres: Array<Genre> }) {
return (
<div className={"flex flex-wrap gap-1 justify-center " + props.className}>
{props.genres.map((e: Genre) => (<Badge className="" key={e.name}>{e.name}</Badge>))}
{props.genres.map((e: Genre) => (<Badge className="text-xs md:text-sm" key={e.name}>{e.name}</Badge>))}
</div>
)
}

View File

@ -45,7 +45,7 @@ import { deleteRecord, deleteRecords } from "app/lib/del"
import { Pathname } from "app/types"
import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTrigger } from "@/components/ui/dialog"
import pluralize from "app/lib/pluralize"
import { updateField } from "app/lib/update"
import { updateField, updatePub, updateStory } from "app/lib/update"
import { tableNameToItemName } from "app/lib/nameMaps"
import { Genre, Pub, Response, Story } from "@prisma/client"
import EditSubmissionDialog from "app/submission/edit"
@ -53,6 +53,7 @@ import { DialogTitle } from "@radix-ui/react-dialog"
import { toast } from "@/components/ui/use-toast"
import { useRouter } from "next/navigation"
import EditStoryDialog from "app/story/edit"
import EditPubDialog from "app/publication/edit"
export interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[]
@ -178,12 +179,19 @@ export function DataTable<TData, TValue>({
/>
: tableName === "story" ?
< EditStoryDialog
dbAction={updateStory}
genres={genres}
// TODO: prepare genre data so that it can be read by StoryForm
defaults={dialogRow?.original}
closeDialog={closeEditDialog}
/>
: ""
: tableName === "pub" ?
<EditPubDialog
dbAction={updatePub}
genres={genres}
defaults={dialogRow?.original}
closeDialog={closeEditDialog}
/>
: ""
}
</DialogContent>
</Dialog>
@ -205,6 +213,7 @@ export function DataTable<TData, TValue>({
const res = await deleteRecord(dialogRow.original.id, pathname)
if (!res) toast({ title: "Oh dear...", description: "Failed to delete." })
if (res) toast({ title: "Successfully deleted record of id:", description: dialogRow.original.id })
table.resetRowSelection()
router.refresh()
}}>Yes, delete it!
</Button>
@ -235,6 +244,7 @@ export function DataTable<TData, TValue>({
const res = await deleteRecords(recordIds, pathname)
if (!res) toast({ title: "Oh dear...", description: "Failed to delete." })
if (res) toast({ title: "Successfully deleted records of id:", description: JSON.stringify(recordIds) })
table.resetRowSelection()
router.refresh()
setIsDeleteDialogVisible(false)
}}>