mirror of
https://github.com/supabase/supabase.git
synced 2026-10-05 09:25:06 +03:00
Chore/server state hooks (#19203)
* add a new component for input * add textArea * some colors * rename * Update page.tsx * move files * Update ChatInput.tsx * Update Header.tsx * chore:upgrade next14 * Update package-lock.json * update graphql * add graphql dep * Update package.json * Update package-lock.json * added server action in * more actions * getting actions in everywhere * Update ConfirmDeleteThreadModal.tsx * Update Input.tsx * update modal action * Bump nextjs in database-new to v14. * fix build * trying server hooks * more experiemntal stuff * update delete * shimmering * update thread UI * Update actions.ts * Update actions.ts * Update Thread.tsx * Update Input.tsx * updated * delete thread updated * fix build * Update package-lock.json * Update edit thread modal * fixing build * reset * Update package-lock.json * Update SpreadsheetImport.utils.ts * reset * Update package-lock.json * Update sitemap_www.xml * pacakge update * change to node 18 * Fix deleting threads --------- Co-authored-by: Terry Sutton <saltcod@gmail.com>
This commit is contained in:
1 parent
356f1ef654
commit
fe053fffb1
12 files changed
+289
-82
No files matched your search
@@ -6,6 +6,7 @@ import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
|
||||
import OpenAI from 'openai'
|
||||
import { z } from 'zod'
|
||||
|
||||
const openai = new OpenAI()
|
||||
|
||||
@@ -24,30 +25,73 @@ export async function logout() {
|
||||
redirect('/')
|
||||
}
|
||||
|
||||
export async function deleteThread(threadID: string) {
|
||||
export async function deleteThread(prevState: any, formData: FormData) {
|
||||
const cookieStore = cookies()
|
||||
const supabase = createClient(cookieStore)
|
||||
|
||||
try {
|
||||
await supabase.from('threads').delete().eq('thread_id', threadID)
|
||||
} catch (error) {
|
||||
if (error) console.error('Error deleting thread: ', error)
|
||||
const schema = z.object({
|
||||
thread_id: z.string(),
|
||||
})
|
||||
|
||||
const data = schema.parse({
|
||||
thread_id: formData.get('thread_id'),
|
||||
})
|
||||
|
||||
await supabase.from('threads').delete().eq('thread_id', data.thread_id)
|
||||
await openai.beta.threads.del(data.thread_id)
|
||||
|
||||
revalidatePath('/profile')
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to delete the thread',
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
await openai.beta.threads.del(threadID)
|
||||
|
||||
revalidatePath('/profile')
|
||||
}
|
||||
|
||||
export async function updateThreadName(threadId: number, threadName: string) {
|
||||
export async function updateThreadName(prevState: any, formData: FormData) {
|
||||
const cookieStore = cookies()
|
||||
const supabase = createClient(cookieStore)
|
||||
|
||||
try {
|
||||
await supabase.from('threads').update({ thread_title: threadName }).eq('id', threadId)
|
||||
} catch (error) {
|
||||
if (error) console.error('Error updating thread: ', error)
|
||||
}
|
||||
const schema = z.object({
|
||||
thread_title: z.string(),
|
||||
row_id: z.string(),
|
||||
})
|
||||
|
||||
revalidatePath('/profile')
|
||||
const data = schema.parse({
|
||||
thread_title: formData.get('thread_title'),
|
||||
row_id: formData.get('row_id'),
|
||||
})
|
||||
|
||||
const { error } = await supabase
|
||||
.from('threads')
|
||||
.update({ thread_title: data.thread_title })
|
||||
.eq('id', data.row_id)
|
||||
if (error) {
|
||||
throw error
|
||||
}
|
||||
revalidatePath('/profile')
|
||||
|
||||
return {
|
||||
message: 'Thread name updated to ' + data.thread_title,
|
||||
success: true,
|
||||
data,
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.error(error)
|
||||
return {
|
||||
success: false,
|
||||
message: 'Failed to update title to update title',
|
||||
data: undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -140,3 +140,34 @@ main {
|
||||
}
|
||||
|
||||
/* .monaco-editor .overflow-guard */
|
||||
|
||||
.shimmering-loader {
|
||||
animation: shimmer 2s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--border-default)) 4%,
|
||||
hsl(var(--background-surface-100)) 25%,
|
||||
hsl(var(--border-default)) 36%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
}
|
||||
|
||||
.dark .shimmering-loader {
|
||||
animation: shimmer 2s infinite linear;
|
||||
background: linear-gradient(
|
||||
to right,
|
||||
hsl(var(--background-surface-100)) 4%,
|
||||
hsl(var(--border-default)) 25%,
|
||||
hsl(var(--background-surface-100)) 36%
|
||||
);
|
||||
background-size: 1000px 100%;
|
||||
}
|
||||
|
||||
@keyframes shimmer {
|
||||
0% {
|
||||
background-position: -1000px 0;
|
||||
}
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { Button, Modal } from 'ui'
|
||||
import { useEffect, createRef } from 'react'
|
||||
import { useFormState, useFormStatus } from 'react-dom'
|
||||
import { Button, Input_Shadcn_, Modal } from 'ui'
|
||||
import { deleteThread } from '@/app/actions'
|
||||
import { ThreadType } from './Threads'
|
||||
import { deleteThread } from '../actions'
|
||||
|
||||
const ConfirmDeleteThreadModal = ({
|
||||
thread,
|
||||
@@ -13,6 +15,46 @@ const ConfirmDeleteThreadModal = ({
|
||||
onClose: () => void
|
||||
visible: boolean
|
||||
}) => {
|
||||
const formRef = createRef<HTMLFormElement>()
|
||||
|
||||
const initialState = {
|
||||
message: '',
|
||||
success: false,
|
||||
data: {
|
||||
thread_id: thread.thread_id,
|
||||
},
|
||||
}
|
||||
|
||||
const [state, formAction] = useFormState(deleteThread, initialState)
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success === true) {
|
||||
onClose()
|
||||
formRef.current?.reset()
|
||||
state.success = false
|
||||
}
|
||||
}, [state, onClose, formRef])
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success === true) {
|
||||
onClose()
|
||||
formRef.current?.reset()
|
||||
state.success = false
|
||||
}
|
||||
}, [state, onClose, formRef])
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<Button type="warning" htmlType="submit" aria-disabled={pending} loading={pending}>
|
||||
Delete thread
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
console.log('thread', thread)
|
||||
|
||||
return (
|
||||
<Modal
|
||||
size="small"
|
||||
@@ -22,16 +64,15 @@ const ConfirmDeleteThreadModal = ({
|
||||
header="Confirm to delete thread?"
|
||||
className="pb-2"
|
||||
>
|
||||
<form action={() => deleteThread(thread.thread_id)}>
|
||||
<form action={formAction} key={`${thread.id}-delete-thread-form`}>
|
||||
<Modal.Content className="py-4">
|
||||
<p className="text-sm">Once the thread is deleted, it cannot be recovered.</p>
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Input_Shadcn_ name="thread_id" required type="hidden" value={thread.thread_id} />
|
||||
<Modal.Content className="flex flex-row gap-3 justify-end">
|
||||
<Button type="default">Cancel</Button>
|
||||
<Button type="warning" htmlType="submit">
|
||||
Delete thread
|
||||
</Button>
|
||||
<SubmitButton />
|
||||
</Modal.Content>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
'use client'
|
||||
|
||||
import { Button, Input, Modal } from 'ui'
|
||||
|
||||
import { updateThreadName } from '@/app/actions'
|
||||
import { createRef, useEffect } from 'react'
|
||||
import { useFormState, useFormStatus } from 'react-dom'
|
||||
import { Button, Input_Shadcn_, Label_Shadcn_, Modal } from 'ui'
|
||||
import { ThreadType } from './Threads'
|
||||
import { useState } from 'react'
|
||||
import { updateThreadName } from '../actions'
|
||||
|
||||
const EditThreadModal = ({
|
||||
thread,
|
||||
@@ -15,7 +15,36 @@ const EditThreadModal = ({
|
||||
onClose: () => void
|
||||
visible: boolean
|
||||
}) => {
|
||||
const [value, setValue] = useState(thread.thread_title)
|
||||
const formRef = createRef<HTMLFormElement>()
|
||||
|
||||
const initialState = {
|
||||
message: undefined,
|
||||
success: undefined,
|
||||
data: {
|
||||
row_id: thread.id,
|
||||
thread_title: thread.thread_title,
|
||||
},
|
||||
}
|
||||
|
||||
const [state, formAction] = useFormState(updateThreadName, initialState)
|
||||
|
||||
useEffect(() => {
|
||||
if (state?.success === true) {
|
||||
onClose()
|
||||
formRef.current?.reset()
|
||||
state.success = undefined
|
||||
}
|
||||
}, [state, onClose, formRef])
|
||||
|
||||
function SubmitButton() {
|
||||
const { pending } = useFormStatus()
|
||||
|
||||
return (
|
||||
<Button type="secondary" htmlType="submit" aria-disabled={pending} loading={pending}>
|
||||
Update thread name
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@@ -27,31 +56,24 @@ const EditThreadModal = ({
|
||||
header="Edit thread name"
|
||||
className="pb-2"
|
||||
>
|
||||
<form
|
||||
action={(formData: FormData) => {
|
||||
const threadNameEntry: FormDataEntryValue | null = formData.get('threadName')
|
||||
|
||||
// Check if threadNameEntry is not null and is of type string
|
||||
if (threadNameEntry !== null && typeof threadNameEntry === 'string') {
|
||||
const threadName: string = threadNameEntry
|
||||
updateThreadName(thread.id, threadName)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<form ref={formRef} action={formAction} key={`${thread.id}-update-thread-title-form`}>
|
||||
<Modal.Content className="py-4">
|
||||
<Input
|
||||
label="Provide a name for your thread"
|
||||
value={value}
|
||||
name="threadName"
|
||||
onChange={(e) => setValue(e.target.value)}
|
||||
<Label_Shadcn_ htmlFor="thread_title">Provide a name for your thread</Label_Shadcn_>
|
||||
<Input_Shadcn_
|
||||
placeholder="Type in a name for the thread..."
|
||||
type="text"
|
||||
name="thread_title"
|
||||
defaultValue={state.data.thread_title}
|
||||
/>
|
||||
<Input_Shadcn_ name="row_id" value={state.data.row_id} type="hidden" />
|
||||
</Modal.Content>
|
||||
<Modal.Separator />
|
||||
<Modal.Content className="flex flex-row gap-3 justify-end">
|
||||
<Button type="default">Cancel</Button>
|
||||
<Button type="secondary" htmlType="submit">
|
||||
Update thread name
|
||||
</Button>
|
||||
<SubmitButton />
|
||||
<p aria-live="polite" className="sr-only" role="status">
|
||||
{state?.message}
|
||||
</p>
|
||||
</Modal.Content>
|
||||
</form>
|
||||
</Modal>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
'use client'
|
||||
|
||||
import Link from 'next/link'
|
||||
import { Button } from 'ui'
|
||||
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
|
||||
import { timeAgo } from '@/lib/utils'
|
||||
import Link from 'next/link'
|
||||
import { useState } from 'react'
|
||||
import {
|
||||
Button,
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
@@ -15,10 +15,9 @@ import {
|
||||
import ConfirmDeleteThreadModal from './ConfirmDeleteThreadModal'
|
||||
import EditThreadModal from './EditThreadModal'
|
||||
import { ThreadType } from './Threads'
|
||||
import { useState } from 'react'
|
||||
|
||||
const Thread = ({ thread }: { thread: ThreadType }) => {
|
||||
const formattedTimeAgo = timeAgo(thread.created_at)
|
||||
const formattedTimeAgo = timeAgo(thread.modified_at)
|
||||
|
||||
const [editOpen, setEditOpen] = useState(false)
|
||||
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||
@@ -27,32 +26,45 @@ const Thread = ({ thread }: { thread: ThreadType }) => {
|
||||
<>
|
||||
<div
|
||||
key={thread.id}
|
||||
className="group flex items-center justify-between border rounded w-full px-4 py-2 transition bg-surface-100 hover:bg-surface-200"
|
||||
className="group w-full flex flex-row items-center gap-6 border rounded pl-5 pr-2 transition bg-surface-100 hover:bg-surface-200 h-14"
|
||||
>
|
||||
<div className="flex flex-col gap-y-1">
|
||||
<Link className="text-sm hover:underline" href={`/${thread.thread_id}/${thread.run_id}`}>
|
||||
{thread.thread_title}
|
||||
<div className="flex flex-col grow overflow-hidden">
|
||||
<Link
|
||||
className="flex text-sm group-hover:underline truncate"
|
||||
href={`/${thread.thread_id}/${thread.run_id}`}
|
||||
>
|
||||
<span className="truncate">{thread.thread_title}</span>
|
||||
</Link>
|
||||
<span className="text-xs text-foreground-light">Last updated {formattedTimeAgo}</span>
|
||||
<span className="text-xs text-foreground-lighter font-mono">
|
||||
Last updated {formattedTimeAgo}
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex shrink">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button className="px-1 group">
|
||||
<IconMoreVertical size={14} className="text-light group-hover:text" />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-32" align="end">
|
||||
<DropdownMenuItem
|
||||
className="space-x-2"
|
||||
onClick={() => {
|
||||
console.log('trying to edit thread')
|
||||
setEditOpen(true)
|
||||
}}
|
||||
>
|
||||
<IconEdit2 size={14} />
|
||||
<p>Edit name</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => setDeleteOpen(true)}>
|
||||
<IconTrash2 size={14} />
|
||||
<p>Delete thread</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button type="text" icon={<IconMoreVertical />} className="px-1" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent className="w-32" align="end">
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => setEditOpen(true)}>
|
||||
<IconEdit2 size={14} />
|
||||
<p>Edit name</p>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="space-x-2" onClick={() => setDeleteOpen(true)}>
|
||||
<IconTrash2 size={14} />
|
||||
<p>Delete thread</p>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
|
||||
<ConfirmDeleteThreadModal
|
||||
key={`${thread.id}-delete-dialog`}
|
||||
thread={thread}
|
||||
|
||||
@@ -26,7 +26,7 @@ async function Threads() {
|
||||
return (
|
||||
<div className="flex flex-col gap-y-3">
|
||||
{threads.length > 0 ? (
|
||||
threads.map((thread) => <Thread key={`thread-item-${thread.id}`} thread={thread} />)
|
||||
threads.sort().map((thread) => <Thread key={`thread-item-${thread.id}`} thread={thread} />)
|
||||
) : (
|
||||
<EmptyState />
|
||||
)}
|
||||
|
||||
@@ -4,6 +4,7 @@ import { cookies } from 'next/headers'
|
||||
import { redirect } from 'next/navigation'
|
||||
import Threads from './Threads'
|
||||
import { Suspense } from 'react'
|
||||
import ShimmeringLoader from '@/components/Shimmer'
|
||||
import type { Metadata } from 'next'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
@@ -45,7 +46,18 @@ const Profile = async () => {
|
||||
|
||||
<div className="w-full h-px border-t" />
|
||||
|
||||
<Suspense fallback={<div>Loading...</div>}>
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex flex-col gap-4">
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-50" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-25" />
|
||||
<ShimmeringLoader className="h-14 bg-surface-100 border rounded opacity-10" />
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<Threads />
|
||||
</Suspense>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
const ANIMATION_DELAY = 150
|
||||
|
||||
const ShimmeringLoader = ({ className = '', delayIndex = 0, animationDelay = 150 }) => {
|
||||
return (
|
||||
<div
|
||||
className={`shimmering-loader rounded py-3 ${className}`}
|
||||
style={{
|
||||
animationFillMode: 'backwards',
|
||||
animationDelay: `${delayIndex * animationDelay}ms`,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
const GenericSkeletonLoader = () => (
|
||||
<div className="space-y-2">
|
||||
<ShimmeringLoader />
|
||||
<ShimmeringLoader className="w-3/4" />
|
||||
<ShimmeringLoader className="w-1/2" />
|
||||
</div>
|
||||
)
|
||||
|
||||
export { GenericSkeletonLoader }
|
||||
export default ShimmeringLoader
|
||||
@@ -20,14 +20,15 @@
|
||||
"eslint-config-next": "^14.0.3",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.292.0",
|
||||
"next": "^14.0.3",
|
||||
"openai": "^4.17.0",
|
||||
"next": "^14.0.3",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
"react-flow": "^1.0.3",
|
||||
"sql-formatter": "^13.1.0",
|
||||
"ui": "*",
|
||||
"valtio": "^1.12.0"
|
||||
"valtio": "^1.12.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.9",
|
||||
|
||||
Vendored
+14
@@ -0,0 +1,14 @@
|
||||
import 'react-dom'
|
||||
|
||||
declare module 'react-dom' {
|
||||
function useFormState<State>(
|
||||
action: (state: State) => Promise<State>,
|
||||
initialState: State,
|
||||
permalink?: string
|
||||
): [state: State, dispatch: () => void]
|
||||
function useFormState<State, Payload>(
|
||||
action: (state: State, payload: Payload) => Promise<State>,
|
||||
initialState: State,
|
||||
permalink?: string
|
||||
): [state: State, dispatch: (payload: Payload) => void]
|
||||
}
|
||||
Generated
+14
-9
@@ -49,7 +49,8 @@
|
||||
"react-flow": "^1.0.3",
|
||||
"sql-formatter": "^13.1.0",
|
||||
"ui": "*",
|
||||
"valtio": "^1.12.0"
|
||||
"valtio": "^1.12.0",
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^18.11.9",
|
||||
@@ -242,8 +243,9 @@
|
||||
}
|
||||
},
|
||||
"apps/database-new/node_modules/@types/node": {
|
||||
"version": "18.18.9",
|
||||
"license": "MIT",
|
||||
"version": "18.18.13",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-18.18.13.tgz",
|
||||
"integrity": "sha512-vXYZGRrSCreZmq1rEjMRLXJhiy8MrIeVasx+PCVlP414N7CJLHnMf+juVvjdprHyH+XRy3zKZLHeNueOpJCn0g==",
|
||||
"dependencies": {
|
||||
"undici-types": "~5.26.4"
|
||||
}
|
||||
@@ -11152,8 +11154,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/@types/react": {
|
||||
"version": "18.2.37",
|
||||
"license": "MIT",
|
||||
"version": "18.2.39",
|
||||
"resolved": "https://registry.npmjs.org/@types/react/-/react-18.2.39.tgz",
|
||||
"integrity": "sha512-Oiw+ppED6IremMInLV4HXGbfbG6GyziY3kqAwJYOR0PNbkYDmLWQA3a95EhdSmamsvbkJN96ZNN+YD+fGjzSBA==",
|
||||
"dependencies": {
|
||||
"@types/prop-types": "*",
|
||||
"@types/scheduler": "*",
|
||||
@@ -11196,9 +11199,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@types/react-dom": {
|
||||
"version": "18.2.8",
|
||||
"version": "18.2.17",
|
||||
"resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.17.tgz",
|
||||
"integrity": "sha512-rvrT/M7Df5eykWFxn6MYt5Pem/Dbyc1N8Y0S9Mrkw2WFCRiqUgw9P7ul2NpwsXCSM1DVdENzdG9J5SreqfAIWg==",
|
||||
"devOptional": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/react": "*"
|
||||
}
|
||||
@@ -36007,8 +36011,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/zod": {
|
||||
"version": "3.22.3",
|
||||
"license": "MIT",
|
||||
"version": "3.22.4",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-3.22.4.tgz",
|
||||
"integrity": "sha512-iC+8Io04lddc+mVqQ9AZ7OQ2MrUKGN+oIQyq1vemgt46jwCwLfhq7/pwnBnNXXXZb8VTVLKwp9EDkx+ryxIWmg==",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user