Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ export default async function ListPage({
}: PageProps<'/dashboard/[teamIdOrSlug]/sandboxes'>) {
const { teamIdOrSlug } = await params

await prefetch(
prefetch(
trpc.sandboxes.getSandboxes.queryOptions({
teamIdOrSlug,
})
Expand Down
1 change: 0 additions & 1 deletion src/app/dashboard/[teamIdOrSlug]/templates/loading.tsx

This file was deleted.

46 changes: 16 additions & 30 deletions src/app/dashboard/[teamIdOrSlug]/templates/page.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,25 @@
import LoadingLayout from '@/features/dashboard/loading-layout'
import TemplatesTable from '@/features/dashboard/templates/table'
import {
getDefaultTemplates,
getTeamTemplates,
} from '@/server/templates/get-team-templates'
import ErrorBoundary from '@/ui/error'
import { HydrateClient, prefetch, trpc } from '@/trpc/server'
import { Suspense } from 'react'

export default async function Page({
params,
}: PageProps<'/dashboard/[teamIdOrSlug]/templates'>) {
const { teamIdOrSlug } = await params

const res = await getTeamTemplates({
teamIdOrSlug,
})
prefetch(
trpc.templates.getTemplates.queryOptions({
teamIdOrSlug,
})
)
prefetch(trpc.templates.getDefaultTemplatesCached.queryOptions())

const defaultRes = await getDefaultTemplates()

if (!res?.data?.templates || res?.serverError) {
return (
<ErrorBoundary
error={
{
name: 'Templates Error',
message: res?.serverError ?? 'Unknown error',
} satisfies Error
}
description={'Could not load templates'}
/>
)
}

const templates = [
...res.data.templates,
...(defaultRes?.data?.templates ? defaultRes.data.templates : []),
]

return <TemplatesTable templates={templates} />
return (
<HydrateClient>
<Suspense fallback={<LoadingLayout />}>
<TemplatesTable />
</Suspense>
</HydrateClient>
)
}
154 changes: 126 additions & 28 deletions src/features/dashboard/templates/table-cells.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,7 @@ import {
} from '@/lib/hooks/use-toast'
import { cn } from '@/lib/utils'
import { isVersionCompatible } from '@/lib/utils/version'
import {
deleteTemplateAction,
updateTemplateAction,
} from '@/server/templates/templates-actions'
import { useTRPC } from '@/trpc/client'
import { DefaultTemplate, Template } from '@/types/api.types'
import { AlertDialog } from '@/ui/alert-dialog'
import { E2BBadge } from '@/ui/brand'
Expand All @@ -26,9 +23,10 @@ import {
DropdownMenuTrigger,
} from '@/ui/primitives/dropdown-menu'
import { Loader } from '@/ui/primitives/loader_d'
import { useMutation, useQueryClient } from '@tanstack/react-query'
import { CellContext } from '@tanstack/react-table'
import { Lock, LockOpen, MoreVertical } from 'lucide-react'
import { useAction } from 'next-safe-action/hooks'
import { useParams } from 'next/navigation'
import { useMemo, useState } from 'react'
import ResourceUsage from '../common/resource-usage'
import { useDashboard } from '../context'
Expand All @@ -49,60 +47,144 @@ export function ActionsCell({
}: CellContext<Template | DefaultTemplate, unknown>) {
const template = row.original
const { team } = useDashboard()
const { teamIdOrSlug } =
useParams<
Awaited<PageProps<'/dashboard/[teamIdOrSlug]/templates'>['params']>
>()

const { toast } = useToast()
const trpc = useTRPC()
const queryClient = useQueryClient()
const [isDeleteDialogOpen, setIsDeleteDialogOpen] = useState(false)

const { execute: executeUpdateTemplate, isExecuting: isUpdating } = useAction(
updateTemplateAction,
{
onSuccess: ({ input }) => {
const updateTemplateMutation = useMutation(
trpc.templates.updateTemplate.mutationOptions({
onSuccess: async (data, variables) => {
const templateName = template.aliases[0] || template.templateID

toast(
defaultSuccessToast(
`Template is now ${input.props.Public ? 'public' : 'private'}.`
<>
Template{' '}
<span className="prose-body-highlight">{templateName}</span> is
now {data.public ? 'public' : 'private'}.
</>
)
)

await queryClient.cancelQueries({
queryKey: trpc.templates.getTemplates.queryKey({
teamIdOrSlug,
}),
})

queryClient.setQueryData(
trpc.templates.getTemplates.queryKey({
teamIdOrSlug,
}),
(old) => {
if (!old?.templates) return old

return {
...old,
templates: old.templates.map((t: Template) =>
t.templateID === variables.templateId
? { ...t, public: variables.public }
: t
),
}
}
)
},
onError: (error) => {
const templateName = template.aliases[0] || template.templateID
toast(
defaultErrorToast(
error.error.serverError || 'Failed to update template.'
error.message || `Failed to update template ${templateName}.`
)
)
},
}
onSettled: () => {
queryClient.invalidateQueries({
queryKey: trpc.templates.getTemplates.queryKey({
teamIdOrSlug,
}),
})
},
})
)

const { execute: executeDeleteTemplate, isExecuting: isDeleting } = useAction(
deleteTemplateAction,
{
onSuccess: () => {
toast(defaultSuccessToast('Template has been deleted.'))
const deleteTemplateMutation = useMutation(
trpc.templates.deleteTemplate.mutationOptions({
onSuccess: async (_, variables) => {
const templateName = template.aliases[0] || template.templateID
toast(
defaultSuccessToast(
<>
Template{' '}
<span className="prose-body-highlight">{templateName}</span> has
been deleted.
</>
)
)

// stop ongoing invlaidations and remove template from state while refetch is going in the background

await queryClient.cancelQueries({
queryKey: trpc.templates.getTemplates.queryKey({
teamIdOrSlug,
}),
})

queryClient.setQueryData(
trpc.templates.getTemplates.queryKey({
teamIdOrSlug,
}),

(old) => {
if (!old?.templates) return old
return {
...old,
templates: old.templates.filter(
(t: Template) => t.templateID !== variables.templateId
),
}
}
)
},
onError: (error) => {
onError: (error, _variables) => {
const templateName = template.aliases[0] || template.templateID
toast(
defaultErrorToast(
error.error.serverError || 'Failed to delete template.'
error.message || `Failed to delete template ${templateName}.`
)
)
},
onSettled: () => {
setIsDeleteDialogOpen(false)

queryClient.invalidateQueries({
queryKey: trpc.templates.getTemplates.queryKey({
teamIdOrSlug,
}),
})
},
}
})
)

const togglePublish = async () => {
executeUpdateTemplate({
const isUpdating = updateTemplateMutation.isPending
const isDeleting = deleteTemplateMutation.isPending

const togglePublish = () => {
updateTemplateMutation.mutate({
teamIdOrSlug: team.slug ?? team.id,
templateId: template.templateID,
props: {
Public: !template.public,
},
public: !template.public,
})
}

const deleteTemplate = async () => {
executeDeleteTemplate({
const deleteTemplate = () => {
deleteTemplateMutation.mutate({
teamIdOrSlug: team.slug ?? team.id,
templateId: template.templateID,
})
Expand All @@ -114,7 +196,23 @@ export function ActionsCell({
open={isDeleteDialogOpen}
onOpenChange={setIsDeleteDialogOpen}
title="Delete Template"
description="Are you sure you want to delete this template? This action cannot be undone."
description={
<>
You are about to delete the template{' '}
{template.aliases[0] && (
<>
<span className="prose-body-highlight">
{template.aliases[0]}
</span>{' '}
(
</>
)}
<code className="text-fg-tertiary font-mono">
{template.templateID}
</code>
{template.aliases[0] && <>)</>}. This action cannot be undone.
</>
}
confirm="Delete"
onConfirm={() => deleteTemplate()}
confirmProps={{
Expand Down
59 changes: 52 additions & 7 deletions src/features/dashboard/templates/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,73 @@
import { useColumnSizeVars } from '@/lib/hooks/use-column-size-vars'
import { useVirtualRows } from '@/lib/hooks/use-virtual-rows'
import { cn } from '@/lib/utils'
import { DefaultTemplate, Template } from '@/types/api.types'
import { useTRPC } from '@/trpc/client'
import { Template } from '@/types/api.types'
import ClientOnly from '@/ui/client-only'
import {
DataTable,
DataTableHead,
DataTableHeader,
DataTableRow,
} from '@/ui/data-table'
import ErrorBoundary from '@/ui/error'
import HelpTooltip from '@/ui/help-tooltip'
import { SIDEBAR_TRANSITION_CLASSNAMES } from '@/ui/primitives/sidebar'
import { useSuspenseQuery } from '@tanstack/react-query'
import {
ColumnFiltersState,
ColumnSizingState,
flexRender,
TableOptions,
useReactTable,
} from '@tanstack/react-table'
import { useEffect, useRef, useState } from 'react'
import { useParams } from 'next/navigation'
import { useEffect, useMemo, useRef, useState } from 'react'
import { useLocalStorage } from 'usehooks-ts'
import TemplatesHeader from './header'
import { useTemplateTableStore } from './stores/table-store'
import { TemplatesTableBody as TableBody } from './table-body'
import { fallbackData, templatesTableConfig, useColumns } from './table-config'

interface TemplatesTableProps {
templates: (Template | DefaultTemplate)[]
}

const ROW_HEIGHT_PX = 32
const VIRTUAL_OVERSCAN = 8

export default function TemplatesTable({ templates }: TemplatesTableProps) {
export default function TemplatesTable() {
'use no memo'

const trpc = useTRPC()
const { teamIdOrSlug } =
useParams<
Awaited<PageProps<'/dashboard/[teamIdOrSlug]/templates'>['params']>
>()

const { data: templatesData, error: templatesError } = useSuspenseQuery(
trpc.templates.getTemplates.queryOptions(
{ teamIdOrSlug },
{
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
}
)
)

const { data: defaultTemplatesData } = useSuspenseQuery(
trpc.templates.getDefaultTemplatesCached.queryOptions(undefined, {
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
})
)

const templates = useMemo(
() => [
...(defaultTemplatesData?.templates ?? []),
...(templatesData?.templates ?? []),
],
[templatesData, defaultTemplatesData]
)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bug: Template display order reversed

The templates array now places default templates before team templates, reversing the original order where team templates appeared first. Since there's no default sorting applied, this changes the display order in the UI. The original code had team templates first, then default templates.

Fix in Cursor Fix in Web


const scrollRef = useRef<HTMLDivElement>(null)

const { sorting, setSorting, globalFilter, setGlobalFilter } =
Expand Down Expand Up @@ -126,6 +159,18 @@ export default function TemplatesTable({ templates }: TemplatesTableProps) {
overscan: VIRTUAL_OVERSCAN,
})

if (templatesError) {
return (
<ErrorBoundary
error={{
name: 'Templates Error',
message: templatesError?.message ?? 'Failed to load templates',
}}
description="Could not load templates"
/>
)
}

return (
<ClientOnly className="flex h-full min-h-0 flex-col md:max-w-[calc(100svw-var(--sidebar-width-active))] p-3 md:p-6">
<TemplatesHeader table={table} />
Expand Down
Loading
Loading