Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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>
)
}
70 changes: 38 additions & 32 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,68 @@ 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: (data) => {
toast(
defaultSuccessToast(
`Template is now ${input.props.Public ? 'public' : 'private'}.`
`Template is now ${data.public ? 'public' : 'private'}.`
)
)
queryClient.invalidateQueries({
queryKey: trpc.templates.getTemplates.queryKey({
teamIdOrSlug,
}),
})
},
onError: (error) => {
toast(
defaultErrorToast(
error.error.serverError || 'Failed to update template.'
)
)
toast(defaultErrorToast(error.message || 'Failed to update template.'))
},
}
})
)

const { execute: executeDeleteTemplate, isExecuting: isDeleting } = useAction(
deleteTemplateAction,
{
const deleteTemplateMutation = useMutation(
trpc.templates.deleteTemplate.mutationOptions({
onSuccess: () => {
toast(defaultSuccessToast('Template has been deleted.'))
queryClient.invalidateQueries({
queryKey: trpc.templates.getTemplates.queryKey({
teamIdOrSlug,
}),
})
},
onError: (error) => {
toast(
defaultErrorToast(
error.error.serverError || 'Failed to delete template.'
)
)
toast(defaultErrorToast(error.message || 'Failed to delete template.'))
},
onSettled: () => {
setIsDeleteDialogOpen(false)
},
}
})
)

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 Down
70 changes: 63 additions & 7 deletions src/features/dashboard/templates/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,40 +3,79 @@
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 LoadingLayout from '../loading-layout'
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,
isFetching: isTemplatesFetching,
} = useSuspenseQuery(
trpc.templates.getTemplates.queryOptions(
{ teamIdOrSlug },
{
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
}
)
)

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

const templates = useMemo(
() => [
...(templatesData?.templates ?? []),
...(defaultTemplatesData?.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 +165,23 @@ export default function TemplatesTable({ templates }: TemplatesTableProps) {
overscan: VIRTUAL_OVERSCAN,
})

if (isTemplatesFetching) {
return <LoadingLayout />
}

if (templatesError || defaultTemplatesError) {
const error = templatesError || defaultTemplatesError
return (
<ErrorBoundary
error={{
name: 'Templates Error',
message: error?.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
11 changes: 11 additions & 0 deletions src/lib/utils/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,14 @@ export function debounce<T extends (...args: any[]) => void>(
}, wait)
}
}

export async function safeCall<T>(
fn: () => Promise<T>
): Promise<[error: null, data: T] | [error: unknown, data: null]> {
try {
const data = await fn()
return [null, data]
} catch (error) {
return [error, null]
}
}
2 changes: 2 additions & 0 deletions src/server/api/routers/index.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import { createCallerFactory, createTRPCRouter } from '../init'
import { sandboxesRouter } from './sandboxes'
import { teamsRouter } from './teams'
import { templatesRouter } from './templates'

export const trpcAppRouter = createTRPCRouter({
sandboxes: sandboxesRouter,
teams: teamsRouter,
templates: templatesRouter,
})

export type TRPCAppRouter = typeof trpcAppRouter
Expand Down
Loading
Loading