begin implementation of edit feature
This commit is contained in:
		
							parent
							
								
									221323ae83
								
							
						
					
					
						commit
						d32b689fbb
					
				
							
								
								
									
										
											BIN
										
									
								
								prisma/dev.db
								
								
								
								
							
							
						
						
									
										
											BIN
										
									
								
								prisma/dev.db
								
								
								
								
							
										
											Binary file not shown.
										
									
								
							|  | @ -1,12 +1,12 @@ | ||||||
| "use server" | "use server" | ||||||
| import { Genre } from "@prisma/client" | import { Genre, Story } from "@prisma/client" | ||||||
| import prisma from "./db" | import prisma from "./db" | ||||||
| import { revalidatePath } from "next/cache" | import { revalidatePath } from "next/cache" | ||||||
| import { redirect } from "next/navigation" | import { redirect } from "next/navigation" | ||||||
| 
 | 
 | ||||||
| export async function createStory(data) { | export async function createStory(data: Story & { genres: number[] }) { | ||||||
| 	"use server" | 	"use server" | ||||||
| 	const genresArray = data.genres.map((e: Genre) => { return { id: e } }) | 	const genresArray = data.genres.map((e) => { return { id: e } }) | ||||||
| 	const res = await prisma.story.create({ | 	const res = await prisma.story.create({ | ||||||
| 		data: { | 		data: { | ||||||
| 			title: data.title, | 			title: data.title, | ||||||
|  |  | ||||||
|  | @ -0,0 +1,32 @@ | ||||||
|  | "use server" | ||||||
|  | import { Genre, Story } from "@prisma/client" | ||||||
|  | import { StoryWithGenres } from "app/story/page" | ||||||
|  | import prisma from "./db" | ||||||
|  | import { revalidatePath } from "next/cache" | ||||||
|  | import { redirect } from "next/navigation" | ||||||
|  | 
 | ||||||
|  | export async function updateStory(data: Story & { genres: number[] }) { | ||||||
|  | 	const genresArray = data.genres.map((e) => { return { id: e } }) | ||||||
|  | 
 | ||||||
|  | 	const res = await prisma.story.update({ | ||||||
|  | 		where: { id: data.id }, | ||||||
|  | 		data: { | ||||||
|  | 			title: data.title, | ||||||
|  | 			word_count: data.word_count, | ||||||
|  | 			genres: { set: genresArray } | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | 	console.log(`updated story: ${res}`) | ||||||
|  | 	revalidatePath("/story") | ||||||
|  | 	redirect("/story") | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | export async function updateTextField({ text, table, column, id }: { text: string, table: string, column: string, id: number }) { | ||||||
|  | 	const res = prisma[table].update({ | ||||||
|  | 		where: { id }, | ||||||
|  | 		data: { | ||||||
|  | 			[column]: text | ||||||
|  | 		} | ||||||
|  | 	}) | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | @ -5,6 +5,7 @@ import { ArrowUpDown } from "lucide-react" | ||||||
| import { Button } from "@/components/ui/button" | import { Button } from "@/components/ui/button" | ||||||
| import GenreBadges from "app/ui/genreBadges" | import GenreBadges from "app/ui/genreBadges" | ||||||
| import { actions } from "app/ui/tables/actions" | import { actions } from "app/ui/tables/actions" | ||||||
|  | import { TextInputCell } from "app/ui/inputs/textInput" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| const columnHelper = createColumnHelper<StoryWithGenres>() | const columnHelper = createColumnHelper<StoryWithGenres>() | ||||||
|  | @ -23,6 +24,7 @@ export const columns: ColumnDef<StoryWithGenres>[] = [ | ||||||
|         </Button> |         </Button> | ||||||
|       ) |       ) | ||||||
|     }, |     }, | ||||||
|  |     cell: TextInputCell | ||||||
| 
 | 
 | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|  |  | ||||||
|  | @ -19,7 +19,7 @@ export default function CreateStoryDialog({ genres }: ComponentProps<"div"> & { | ||||||
|           <DialogTitle>New story</DialogTitle> |           <DialogTitle>New story</DialogTitle> | ||||||
|           <DialogDescription>Create an entry for a new story i.e. a thing you intend to submit for publication.</DialogDescription> |           <DialogDescription>Create an entry for a new story i.e. a thing you intend to submit for publication.</DialogDescription> | ||||||
|         </DialogHeader> |         </DialogHeader> | ||||||
|         <StoryForm createStory={createStory} genres={genres} /> |         <StoryForm createStory={createStory} genres={genres} existingData={null} /> | ||||||
|         <DialogFooter> |         <DialogFooter> | ||||||
|           <Button form="storyform">Submit</Button> |           <Button form="storyform">Submit</Button> | ||||||
|         </DialogFooter> |         </DialogFooter> | ||||||
|  |  | ||||||
|  | @ -23,6 +23,7 @@ export default async function Page() { | ||||||
|     <div className="container mx-auto"> |     <div className="container mx-auto"> | ||||||
|       <DataTable columns={columns} data={storiesWithGenres} type="story"> |       <DataTable columns={columns} data={storiesWithGenres} type="story"> | ||||||
|         <CreateStoryDialog genres={genres} /> |         <CreateStoryDialog genres={genres} /> | ||||||
|  |         {/* TODO - EDIT STORY DIALOG */} | ||||||
|       </DataTable> |       </DataTable> | ||||||
|     </div> |     </div> | ||||||
|   ) |   ) | ||||||
|  |  | ||||||
|  | @ -1072,6 +1072,10 @@ body { | ||||||
|   align-items: flex-start; |   align-items: flex-start; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .items-end { | ||||||
|  |   align-items: flex-end; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .items-center { | .items-center { | ||||||
|   align-items: center; |   align-items: center; | ||||||
| } | } | ||||||
|  | @ -1427,6 +1431,10 @@ body { | ||||||
|   padding-top: 0.25rem; |   padding-top: 0.25rem; | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | .pt-2 { | ||||||
|  |   padding-top: 0.5rem; | ||||||
|  | } | ||||||
|  | 
 | ||||||
| .text-left { | .text-left { | ||||||
|   text-align: left; |   text-align: left; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -21,31 +21,33 @@ import { | ||||||
| 	PopoverContent, | 	PopoverContent, | ||||||
| } from "@/components/ui/popover" | } from "@/components/ui/popover" | ||||||
| import { ComponentProps } from "react" | import { ComponentProps } from "react" | ||||||
| import { Genre } from "@prisma/client" | import { Genre, Story } from "@prisma/client" | ||||||
| import { randomStoryTitle } from "app/lib/shortStoryTitleGenerator" | import { randomStoryTitle } from "app/lib/shortStoryTitleGenerator" | ||||||
| import { usePathname } from "next/navigation" | import { usePathname } from "next/navigation" | ||||||
| import GenrePicker from "./genrePicker" | import GenrePicker from "./genrePicker" | ||||||
|  | import { StoryWithGenres } from "app/story/page" | ||||||
| 
 | 
 | ||||||
| const formSchema = z.object({ | const formSchema = z.object({ | ||||||
|  | 	id: z.number().optional(), | ||||||
| 	title: z.string().min(2).max(50), | 	title: z.string().min(2).max(50), | ||||||
| 	word_count: z.coerce.number().min(100), | 	word_count: z.coerce.number().min(100), | ||||||
| 	genres: z.array(z.number()) | 	genres: z.array(z.number()) | ||||||
| }) | }) | ||||||
| 
 | 
 | ||||||
| export default function StoryForm({ genres, createStory, className }: ComponentProps<"div"> & { genres: Array<Genre>, createStory: (data: any) => void }) { | export default function StoryForm({ genres, createStory, className, existingData }: ComponentProps<"div"> & { genres: Array<Genre>, createStory: (data: any) => void, existingData: Story & { genres: number[] } | null }) { | ||||||
| 	// 1. Define your form.
 |  | ||||||
| 	const form = useForm<z.infer<typeof formSchema>>({ | 	const form = useForm<z.infer<typeof formSchema>>({ | ||||||
| 		resolver: zodResolver(formSchema), | 		resolver: zodResolver(formSchema), | ||||||
| 		defaultValues: { | 		defaultValues: { | ||||||
| 			title: "", | 			id: existingData?.id, | ||||||
| 			word_count: 0, | 			title: existingData?.title ?? "", | ||||||
| 			genres: [] | 			word_count: existingData?.word_count ?? 500, | ||||||
|  | 			genres: existingData?.genres ?? [] | ||||||
| 		}, | 		}, | ||||||
| 	}) | 	}) | ||||||
| 	// 2. Define a submit handler.
 | 
 | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| 	function onSubmit(values: z.infer<typeof formSchema>) { | 	function onSubmit(values: z.infer<typeof formSchema>) { | ||||||
| 		// Do something with the form values.
 |  | ||||||
| 		// ✅ This will be type-safe and validated.
 |  | ||||||
| 		toast({ | 		toast({ | ||||||
| 			title: "You submitted the following values:", | 			title: "You submitted the following values:", | ||||||
| 			description: ( | 			description: ( | ||||||
|  |  | ||||||
|  | @ -0,0 +1,34 @@ | ||||||
|  | import { Cell, Table } from "@tanstack/react-table" | ||||||
|  | import { useState, useEffect } from "react" | ||||||
|  | import { Input } from "@/components/ui/input" | ||||||
|  | import { Button } from "@/components/ui/button" | ||||||
|  | import { Check, CircleX } from "lucide-react" | ||||||
|  | 
 | ||||||
|  | export const TextInputCell = ({ getValue, row }) => { | ||||||
|  |   const initialValue = getValue() | ||||||
|  |   useEffect(() => { | ||||||
|  |     setValue(initialValue) | ||||||
|  |   }, [initialValue]) | ||||||
|  |   const [value, setValue] = useState("") | ||||||
|  |   const [isActive, setIsActive] = useState(false) | ||||||
|  | 
 | ||||||
|  |   return (<div | ||||||
|  |     onDoubleClick={() => setIsActive(prev => !prev)} | ||||||
|  |   > | ||||||
|  |     {isActive ? | ||||||
|  |       <div className="flex flex-col"> | ||||||
|  |         <Input | ||||||
|  |           value={value} | ||||||
|  |           onChange={e => setValue(e.target.value)} | ||||||
|  |           onBlur={() => setIsActive(false)} | ||||||
|  |           autoFocus={true} | ||||||
|  |         /> | ||||||
|  |         <div className="flex flex-row justify-end gap-1 w-full pt-2"> | ||||||
|  |           <Button variant="outline"><Check /></Button> | ||||||
|  |           <Button variant="outline"><CircleX /></Button> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       : <p>{value}</p> | ||||||
|  |     } | ||||||
|  |   </div>) | ||||||
|  | } | ||||||
|  | @ -13,9 +13,12 @@ export default function FormContextMenu({ pathname, row, selectedRows, deselect | ||||||
|     <Dialog modal={true}> |     <Dialog modal={true}> | ||||||
|       <ContextMenuContent > |       <ContextMenuContent > | ||||||
|         {pathname !== "/submission" && selectedRows.length <= 1 ? |         {pathname !== "/submission" && selectedRows.length <= 1 ? | ||||||
|  |           <> | ||||||
|             <Link href={`${pathname}/${row.original.id}`}> |             <Link href={`${pathname}/${row.original.id}`}> | ||||||
|               <ContextMenuItem>Inspect</ContextMenuItem> |               <ContextMenuItem>Inspect</ContextMenuItem> | ||||||
|             </Link> |             </Link> | ||||||
|  |             <ContextMenuItem>Edit</ContextMenuItem> | ||||||
|  |           </> | ||||||
|           : "" |           : "" | ||||||
|         } |         } | ||||||
|         { |         { | ||||||
|  |  | ||||||
|  | @ -44,6 +44,7 @@ import { deleteRecords } from "app/lib/del" | ||||||
| import { Pathname } from "app/types" | import { Pathname } from "app/types" | ||||||
| import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTrigger } from "@/components/ui/dialog" | import { Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTrigger } from "@/components/ui/dialog" | ||||||
| import pluralize from "app/lib/pluralize" | import pluralize from "app/lib/pluralize" | ||||||
|  | import { updateTextField } from "app/lib/update" | ||||||
| 
 | 
 | ||||||
| interface DataTableProps<TData, TValue> { | interface DataTableProps<TData, TValue> { | ||||||
|   columns: ColumnDef<TData, TValue>[] |   columns: ColumnDef<TData, TValue>[] | ||||||
|  | @ -55,7 +56,7 @@ export function DataTable<TData, TValue>({ | ||||||
|   columns, |   columns, | ||||||
|   data, |   data, | ||||||
|   children |   children | ||||||
| }: DataTableProps<TData, TValue> & ComponentProps<"div"> & { type: "publication" | "submission" | "story" | "genre" | "response" }) { | }: DataTableProps<TData, TValue> & ComponentProps<"div">) { | ||||||
|   //STATE
 |   //STATE
 | ||||||
|   const [sorting, setSorting] = useState<SortingState>([]) |   const [sorting, setSorting] = useState<SortingState>([]) | ||||||
|   const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>( |   const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>( | ||||||
|  | @ -80,6 +81,12 @@ export function DataTable<TData, TValue>({ | ||||||
|       sorting, |       sorting, | ||||||
|       columnFilters, |       columnFilters, | ||||||
|       columnVisibility, |       columnVisibility, | ||||||
|  |     }, | ||||||
|  |     //this is where you put arbitrary functions etc to make them accessible via the table api
 | ||||||
|  |     meta: { | ||||||
|  |       updateTextField, | ||||||
|  |       //TODO - move select row action here so it can be accessed from within a cell
 | ||||||
|  |       selectRow: () => { } | ||||||
|     } |     } | ||||||
|   }) |   }) | ||||||
| 
 | 
 | ||||||
|  | @ -247,6 +254,7 @@ export function DataTable<TData, TValue>({ | ||||||
|           size="sm" |           size="sm" | ||||||
|           onClick={() => table.previousPage()} |           onClick={() => table.previousPage()} | ||||||
|           disabled={!table.getCanPreviousPage()} |           disabled={!table.getCanPreviousPage()} | ||||||
|  |           onBlur={true} | ||||||
|         > |         > | ||||||
|           Previous |           Previous | ||||||
|         </Button> |         </Button> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue