chore(templates): disable templates page and related UI (#3690)

* chore(templates): disable templates page and related UI

* chore(templates): remove unused imports from disabled template code

* fix(config): restore noNestedComponentDefinitions rule in biome config

* chore(templates): comment out remaining dead template code

Comment out handleTemplateFormSubmit, handleTemplateDelete,
TemplateStatusBadge component, and TemplateProfile dynamic import
that were left over after disabling the templates feature.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(templates): clean up dead code from review feedback

- Remove unused usePathname/pathnameRef in use-workspace-management.ts
- Comment out stale 'template' from TabView union type
- Remove unused params from TemplateLayoutProps interface

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Waleed
2026-03-19 23:43:11 -07:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 1eb85dd66f
commit e796dfee0d
17 changed files with 394 additions and 614 deletions
@@ -23,7 +23,7 @@ const PRODUCT_LINKS: FooterItem[] = [
const RESOURCES_LINKS: FooterItem[] = [
{ label: 'Blog', href: '/blog' },
{ label: 'Templates', href: '/templates' },
// { label: 'Templates', href: '/templates' },
{ label: 'Docs', href: 'https://docs.sim.ai', external: true },
{ label: 'Careers', href: 'https://jobs.ashbyhq.com/sim', external: true },
{ label: 'Changelog', href: '/changelog' },
-1
View File
@@ -12,7 +12,6 @@ Sim lets teams create agents, workflows, knowledge bases, tables, and docs. Over
## Core Pages
- [Homepage](${baseUrl}): Product overview, features, and pricing
- [Templates](${baseUrl}/templates): Pre-built workflow templates to get started quickly
- [Changelog](${baseUrl}/changelog): Product updates and release notes
- [Sim Blog](${baseUrl}/blog): Announcements, insights, and guides
+4 -4
View File
@@ -20,10 +20,10 @@ export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
url: `${baseUrl}/blog/tags`,
lastModified: now,
},
{
url: `${baseUrl}/templates`,
lastModified: now,
},
// {
// url: `${baseUrl}/templates`,
// lastModified: now,
// },
{
url: `${baseUrl}/changelog`,
lastModified: now,
+10 -34
View File
@@ -1,44 +1,20 @@
import { db } from '@sim/db'
import { permissions, workspace } from '@sim/db/schema'
import { and, desc, eq } from 'drizzle-orm'
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
// import { db } from '@sim/db'
// import { permissions, workspace } from '@sim/db/schema'
// import { and, desc, eq } from 'drizzle-orm'
// import { redirect } from 'next/navigation'
// import { getSession } from '@/lib/auth'
export const dynamic = 'force-dynamic'
export const revalidate = 0
// export const dynamic = 'force-dynamic'
// export const revalidate = 0
interface TemplateLayoutProps {
children: React.ReactNode
params: Promise<{
id: string
}>
}
/**
* Template detail layout (public scope).
* - If user is authenticated, redirect to workspace-scoped template detail.
* - Otherwise render the public template detail children.
* Template detail layout (public scope) — currently disabled.
* Previously redirected authenticated users to the workspace-scoped template detail.
*/
export default async function TemplateDetailLayout({ children, params }: TemplateLayoutProps) {
const { id } = await params
const session = await getSession()
if (session?.user?.id) {
const userWorkspaces = await db
.select({
workspace: workspace,
})
.from(permissions)
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
.where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
.orderBy(desc(workspace.createdAt))
.limit(1)
if (userWorkspaces.length > 0) {
const firstWorkspace = userWorkspaces[0].workspace
redirect(`/workspace/${firstWorkspace.id}/templates/${id}`)
}
}
export default function TemplateDetailLayout({ children }: TemplateLayoutProps) {
return children
}
+87 -86
View File
@@ -1,92 +1,93 @@
import { db } from '@sim/db'
import { templateCreators, templates } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import type { Metadata } from 'next'
import { getBaseUrl } from '@/lib/core/utils/urls'
import TemplateDetails from '@/app/templates/[id]/template'
import { notFound } from 'next/navigation'
const logger = createLogger('TemplateMetadata')
// import { db } from '@sim/db'
// import { templateCreators, templates } from '@sim/db/schema'
// import { createLogger } from '@sim/logger'
// import { eq } from 'drizzle-orm'
// import type { Metadata } from 'next'
// import { getBaseUrl } from '@/lib/core/utils/urls'
// import TemplateDetails from '@/app/templates/[id]/template'
// const logger = createLogger('TemplateMetadata')
// /**
// * Generate dynamic metadata for template pages.
// * This provides OpenGraph images for social media sharing.
// */
// export async function generateMetadata({
// params,
// }: {
// params: Promise<{ id: string }>
// }): Promise<Metadata> {
// const { id } = await params
//
// try {
// const result = await db
// .select({
// template: templates,
// creator: templateCreators,
// })
// .from(templates)
// .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
// .where(eq(templates.id, id))
// .limit(1)
//
// if (result.length === 0) {
// return {
// title: 'Template Not Found',
// description: 'The requested template could not be found.',
// }
// }
//
// const { template, creator } = result[0]
// const baseUrl = getBaseUrl()
//
// const details = template.details as { tagline?: string; about?: string } | null
// const description = details?.tagline || 'AI workflow template on Sim'
//
// const hasOgImage = !!template.ogImageUrl
// const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
//
// return {
// title: template.name,
// description,
// openGraph: {
// title: template.name,
// description,
// type: 'website',
// url: `${baseUrl}/templates/${id}`,
// siteName: 'Sim',
// images: [
// {
// url: ogImageUrl,
// width: hasOgImage ? 1200 : 512,
// height: hasOgImage ? 630 : 512,
// alt: `${template.name} - Workflow Preview`,
// },
// ],
// },
// twitter: {
// card: hasOgImage ? 'summary_large_image' : 'summary',
// title: template.name,
// description,
// images: [ogImageUrl],
// creator: creator?.details
// ? ((creator.details as Record<string, unknown>).xHandle as string) || undefined
// : undefined,
// },
// }
// } catch (error) {
// logger.error('Failed to generate template metadata:', error)
// return {
// title: 'Template',
// description: 'AI workflow template on Sim',
// }
// }
// }
/**
* Generate dynamic metadata for template pages.
* This provides OpenGraph images for social media sharing.
*/
export async function generateMetadata({
params,
}: {
params: Promise<{ id: string }>
}): Promise<Metadata> {
const { id } = await params
try {
const result = await db
.select({
template: templates,
creator: templateCreators,
})
.from(templates)
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
.where(eq(templates.id, id))
.limit(1)
if (result.length === 0) {
return {
title: 'Template Not Found',
description: 'The requested template could not be found.',
}
}
const { template, creator } = result[0]
const baseUrl = getBaseUrl()
const details = template.details as { tagline?: string; about?: string } | null
const description = details?.tagline || 'AI workflow template on Sim'
const hasOgImage = !!template.ogImageUrl
const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
return {
title: template.name,
description,
openGraph: {
title: template.name,
description,
type: 'website',
url: `${baseUrl}/templates/${id}`,
siteName: 'Sim',
images: [
{
url: ogImageUrl,
width: hasOgImage ? 1200 : 512,
height: hasOgImage ? 630 : 512,
alt: `${template.name} - Workflow Preview`,
},
],
},
twitter: {
card: hasOgImage ? 'summary_large_image' : 'summary',
title: template.name,
description,
images: [ogImageUrl],
creator: creator?.details
? ((creator.details as Record<string, unknown>).xHandle as string) || undefined
: undefined,
},
}
} catch (error) {
logger.error('Failed to generate template metadata:', error)
return {
title: 'Template',
description: 'AI workflow template on Sim',
}
}
}
/**
* Public template detail page for unauthenticated users.
* Authenticated-user redirect is handled in templates/[id]/layout.tsx.
* Public template detail page — currently disabled, returns 404.
*/
export default function TemplatePage() {
return <TemplateDetails />
notFound()
}
+72 -66
View File
@@ -1,73 +1,79 @@
import { db } from '@sim/db'
import { permissions, templateCreators, templates, workspace } from '@sim/db/schema'
import { and, desc, eq } from 'drizzle-orm'
import type { Metadata } from 'next'
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import type { Template } from '@/app/templates/templates'
import Templates from '@/app/templates/templates'
import { notFound } from 'next/navigation'
export const metadata: Metadata = {
title: 'Templates',
description:
'Browse pre-built workflow templates to get started quickly with AI agents, automations, and integrations.',
}
// import { db } from '@sim/db'
// import { permissions, templateCreators, templates, workspace } from '@sim/db/schema'
// import { and, desc, eq } from 'drizzle-orm'
// import type { Metadata } from 'next'
// import { redirect } from 'next/navigation'
// import { getSession } from '@/lib/auth'
// import type { Template } from '@/app/templates/templates'
// import Templates from '@/app/templates/templates'
// export const metadata: Metadata = {
// title: 'Templates',
// description:
// 'Browse pre-built workflow templates to get started quickly with AI agents, automations, and integrations.',
// }
/**
* Public templates list page.
* Redirects authenticated users to their workspace-scoped templates page.
* Allows unauthenticated users to view templates for SEO and discovery.
* Currently disabled — returns 404.
*/
export default async function TemplatesPage() {
const session = await getSession()
export default function TemplatesPage() {
notFound()
// Authenticated users: redirect to workspace-scoped templates
if (session?.user?.id) {
const userWorkspaces = await db
.select({
workspace: workspace,
})
.from(permissions)
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
.where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
.orderBy(desc(workspace.createdAt))
.limit(1)
if (userWorkspaces.length > 0) {
const firstWorkspace = userWorkspaces[0].workspace
redirect(`/workspace/${firstWorkspace.id}/templates`)
}
}
// Unauthenticated users: show public templates
const templatesData = await db
.select({
id: templates.id,
workflowId: templates.workflowId,
name: templates.name,
details: templates.details,
creatorId: templates.creatorId,
creator: templateCreators,
views: templates.views,
stars: templates.stars,
status: templates.status,
tags: templates.tags,
requiredCredentials: templates.requiredCredentials,
state: templates.state,
createdAt: templates.createdAt,
updatedAt: templates.updatedAt,
})
.from(templates)
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
.where(eq(templates.status, 'approved'))
.orderBy(desc(templates.views), desc(templates.createdAt))
.then((rows) => rows.map((row) => ({ ...row, isStarred: false })))
return (
<Templates
initialTemplates={templatesData as unknown as Template[]}
currentUserId={null}
isSuperUser={false}
/>
)
// Redirects authenticated users to their workspace-scoped templates page.
// Allows unauthenticated users to view templates for SEO and discovery.
//
// const session = await getSession()
//
// // Authenticated users: redirect to workspace-scoped templates
// if (session?.user?.id) {
// const userWorkspaces = await db
// .select({
// workspace: workspace,
// })
// .from(permissions)
// .innerJoin(workspace, eq(permissions.entityId, workspace.id))
// .where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
// .orderBy(desc(workspace.createdAt))
// .limit(1)
//
// if (userWorkspaces.length > 0) {
// const firstWorkspace = userWorkspaces[0].workspace
// redirect(`/workspace/${firstWorkspace.id}/templates`)
// }
// }
//
// // Unauthenticated users: show public templates
// const templatesData = await db
// .select({
// id: templates.id,
// workflowId: templates.workflowId,
// name: templates.name,
// details: templates.details,
// creatorId: templates.creatorId,
// creator: templateCreators,
// views: templates.views,
// stars: templates.stars,
// status: templates.status,
// tags: templates.tags,
// requiredCredentials: templates.requiredCredentials,
// state: templates.state,
// createdAt: templates.createdAt,
// updatedAt: templates.updatedAt,
// })
// .from(templates)
// .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
// .where(eq(templates.status, 'approved'))
// .orderBy(desc(templates.views), desc(templates.createdAt))
// .then((rows) => rows.map((row) => ({ ...row, isStarred: false })))
//
// return (
// <Templates
// initialTemplates={templatesData as unknown as Template[]}
// currentUserId={null}
// isSuperUser={false}
// />
// )
}
@@ -8,7 +8,6 @@ import { getDocumentIcon } from '@/components/icons/document-icons'
import { useSession } from '@/lib/auth/auth-client'
import {
LandingPromptStorage,
LandingTemplateStorage,
type LandingWorkflowSeed,
LandingWorkflowSeedStorage,
} from '@/lib/core/utils/browser-storage'
@@ -118,12 +117,12 @@ export function Home({ chatId }: HomeProps = {}) {
return
}
const templateId = LandingTemplateStorage.consume()
if (templateId) {
logger.info('Retrieved landing page template, redirecting to template detail')
router.replace(`/workspace/${workspaceId}/templates/${templateId}?use=true`)
return
}
// const templateId = LandingTemplateStorage.consume()
// if (templateId) {
// logger.info('Retrieved landing page template, redirecting to template detail')
// router.replace(`/workspace/${workspaceId}/templates/${templateId}?use=true`)
// return
// }
const prompt = LandingPromptStorage.consume()
if (prompt) {
@@ -57,13 +57,13 @@ const Credentials = dynamic(
),
{ loading: () => <CredentialsSkeleton /> }
)
const TemplateProfile = dynamic(
() =>
import(
'@/app/workspace/[workspaceId]/settings/components/template-profile/template-profile'
).then((m) => m.TemplateProfile),
{ loading: () => <SettingsSectionSkeleton /> }
)
// const TemplateProfile = dynamic(
// () =>
// import(
// '@/app/workspace/[workspaceId]/settings/components/template-profile/template-profile'
// ).then((m) => m.TemplateProfile),
// { loading: () => <SettingsSectionSkeleton /> }
// )
const CredentialSets = dynamic(
() =>
import(
@@ -177,7 +177,7 @@ export function SettingsPage({ section }: SettingsPageProps) {
{effectiveSection === 'general' && <General />}
{effectiveSection === 'integrations' && <Integrations />}
{effectiveSection === 'secrets' && <Credentials />}
{effectiveSection === 'template-profile' && <TemplateProfile />}
{/* {effectiveSection === 'template-profile' && <TemplateProfile />} */}
{effectiveSection === 'credential-sets' && <CredentialSets />}
{effectiveSection === 'access-control' && <AccessControl />}
{effectiveSection === 'apikeys' && <ApiKeys />}
@@ -14,7 +14,6 @@ import {
ShieldCheck,
TerminalWindow,
TrashOutline,
User,
Users,
Wrench,
} from '@/components/emcn'
@@ -84,7 +83,7 @@ export const sectionConfig: { key: NavigationSection; title: string }[] = [
export const allNavigationItems: NavigationItem[] = [
{ id: 'general', label: 'General', icon: Settings, section: 'account' },
{ id: 'template-profile', label: 'Template Profile', icon: User, section: 'account' },
// { id: 'template-profile', label: 'Template Profile', icon: User, section: 'account' },
{
id: 'access-control',
label: 'Access Control',
@@ -1,115 +1,103 @@
import { db } from '@sim/db'
import { templateCreators, templates } from '@sim/db/schema'
import { createLogger } from '@sim/logger'
import { eq } from 'drizzle-orm'
import type { Metadata } from 'next'
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { getBaseUrl } from '@/lib/core/utils/urls'
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
import TemplateDetails from '@/app/templates/[id]/template'
import { notFound } from 'next/navigation'
const logger = createLogger('WorkspaceTemplateMetadata')
// import { db } from '@sim/db'
// import { templateCreators, templates } from '@sim/db/schema'
// import { createLogger } from '@sim/logger'
// import { eq } from 'drizzle-orm'
// import type { Metadata } from 'next'
// import { redirect } from 'next/navigation'
// import { getSession } from '@/lib/auth'
// import { getBaseUrl } from '@/lib/core/utils/urls'
// import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
// import TemplateDetails from '@/app/templates/[id]/template'
interface TemplatePageProps {
params: Promise<{
workspaceId: string
id: string
}>
}
// const logger = createLogger('WorkspaceTemplateMetadata')
// interface TemplatePageProps {
// params: Promise<{
// workspaceId: string
// id: string
// }>
// }
// /**
// * Generate dynamic metadata for workspace template pages.
// * This provides OpenGraph images for social media sharing.
// */
// export async function generateMetadata({
// params,
// }: {
// params: Promise<{ workspaceId: string; id: string }>
// }): Promise<Metadata> {
// const { workspaceId, id } = await params
//
// try {
// const result = await db
// .select({
// template: templates,
// creator: templateCreators,
// })
// .from(templates)
// .leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
// .where(eq(templates.id, id))
// .limit(1)
//
// if (result.length === 0) {
// return {
// title: 'Template Not Found',
// description: 'The requested template could not be found.',
// }
// }
//
// const { template, creator } = result[0]
// const baseUrl = getBaseUrl()
//
// const details = template.details as { tagline?: string; about?: string } | null
// const description = details?.tagline || 'AI workflow template on Sim'
//
// const hasOgImage = !!template.ogImageUrl
// const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
//
// return {
// title: template.name,
// description,
// openGraph: {
// title: template.name,
// description,
// type: 'website',
// url: `${baseUrl}/workspace/${workspaceId}/templates/${id}`,
// siteName: 'Sim',
// images: [
// {
// url: ogImageUrl,
// width: hasOgImage ? 1200 : 512,
// height: hasOgImage ? 630 : 512,
// alt: `${template.name} - Workflow Preview`,
// },
// ],
// },
// twitter: {
// card: hasOgImage ? 'summary_large_image' : 'summary',
// title: template.name,
// description,
// images: [ogImageUrl],
// creator: creator?.details
// ? ((creator.details as Record<string, unknown>).xHandle as string) || undefined
// : undefined,
// },
// }
// } catch (error) {
// logger.error('Failed to generate workspace template metadata:', error)
// return {
// title: 'Template',
// description: 'AI workflow template on Sim',
// }
// }
// }
/**
* Generate dynamic metadata for workspace template pages.
* This provides OpenGraph images for social media sharing.
* Workspace-scoped template detail page — currently disabled, returns 404.
*/
export async function generateMetadata({
params,
}: {
params: Promise<{ workspaceId: string; id: string }>
}): Promise<Metadata> {
const { workspaceId, id } = await params
try {
const result = await db
.select({
template: templates,
creator: templateCreators,
})
.from(templates)
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
.where(eq(templates.id, id))
.limit(1)
if (result.length === 0) {
return {
title: 'Template Not Found',
description: 'The requested template could not be found.',
}
}
const { template, creator } = result[0]
const baseUrl = getBaseUrl()
const details = template.details as { tagline?: string; about?: string } | null
const description = details?.tagline || 'AI workflow template on Sim'
const hasOgImage = !!template.ogImageUrl
const ogImageUrl = template.ogImageUrl || `${baseUrl}/logo/primary/rounded.png`
return {
title: template.name,
description,
openGraph: {
title: template.name,
description,
type: 'website',
url: `${baseUrl}/workspace/${workspaceId}/templates/${id}`,
siteName: 'Sim',
images: [
{
url: ogImageUrl,
width: hasOgImage ? 1200 : 512,
height: hasOgImage ? 630 : 512,
alt: `${template.name} - Workflow Preview`,
},
],
},
twitter: {
card: hasOgImage ? 'summary_large_image' : 'summary',
title: template.name,
description,
images: [ogImageUrl],
creator: creator?.details
? ((creator.details as Record<string, unknown>).xHandle as string) || undefined
: undefined,
},
}
} catch (error) {
logger.error('Failed to generate workspace template metadata:', error)
return {
title: 'Template',
description: 'AI workflow template on Sim',
}
}
}
/**
* Workspace-scoped template detail page.
* Requires authentication and workspace membership to access.
* Uses the shared TemplateDetails component with workspace context.
*/
export default async function TemplatePage({ params }: TemplatePageProps) {
const { workspaceId, id } = await params
const session = await getSession()
if (!session?.user?.id) {
redirect(`/templates/${id}`)
}
const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId)
if (!hasPermission) {
redirect('/')
}
return <TemplateDetails isWorkspaceContext={true} />
export default function TemplatePage() {
notFound()
}
@@ -1,205 +1,29 @@
import { db } from '@sim/db'
import { settings, templateCreators, templateStars, templates, user } from '@sim/db/schema'
import { and, desc, eq, sql } from 'drizzle-orm'
import type { Metadata } from 'next'
import { redirect } from 'next/navigation'
import { getSession } from '@/lib/auth'
import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
import type { Template as WorkspaceTemplate } from '@/app/workspace/[workspaceId]/templates/templates'
import Templates from '@/app/workspace/[workspaceId]/templates/templates'
import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
import { notFound } from 'next/navigation'
export const metadata: Metadata = {
title: 'Templates',
}
// import { db } from '@sim/db'
// import { settings, templateCreators, templateStars, templates, user } from '@sim/db/schema'
// import { and, desc, eq, sql } from 'drizzle-orm'
// import type { Metadata } from 'next'
// import { redirect } from 'next/navigation'
// import { getSession } from '@/lib/auth'
// import { verifyWorkspaceMembership } from '@/app/api/workflows/utils'
// import type { Template as WorkspaceTemplate } from '@/app/workspace/[workspaceId]/templates/templates'
// import Templates from '@/app/workspace/[workspaceId]/templates/templates'
// import { getUserPermissionConfig } from '@/ee/access-control/utils/permission-check'
interface TemplatesPageProps {
params: Promise<{
workspaceId: string
}>
}
// export const metadata: Metadata = {
// title: 'Templates',
// }
// interface TemplatesPageProps {
// params: Promise<{
// workspaceId: string
// }>
// }
/**
* Workspace-scoped Templates page.
* Requires authentication and workspace membership to access.
* Workspace-scoped Templates page — currently disabled, returns 404.
*/
export default async function TemplatesPage({ params }: TemplatesPageProps) {
const { workspaceId } = await params
const session = await getSession()
// Redirect unauthenticated users to public templates page
if (!session?.user?.id) {
redirect('/templates')
}
// Verify workspace membership
const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId)
if (!hasPermission) {
redirect('/')
}
// Check permission group restrictions
const permissionConfig = await getUserPermissionConfig(session.user.id)
if (permissionConfig?.hideTemplates) {
redirect(`/workspace/${workspaceId}`)
}
// Determine effective super user (admin role AND UI mode enabled)
const currentUser = await db
.select({ role: user.role })
.from(user)
.where(eq(user.id, session.user.id))
.limit(1)
const userSettings = await db
.select({ superUserModeEnabled: settings.superUserModeEnabled })
.from(settings)
.where(eq(settings.userId, session.user.id))
.limit(1)
const isSuperUser = currentUser[0]?.role === 'admin'
const superUserModeEnabled = userSettings[0]?.superUserModeEnabled ?? false
const effectiveSuperUser = isSuperUser && superUserModeEnabled
// Load templates from database
let rows:
| Array<{
id: string
workflowId: string | null
name: string
details?: unknown
creatorId: string | null
creator: {
id: string
referenceType: 'user' | 'organization'
referenceId: string
name: string
profileImageUrl?: string | null
details?: unknown
verified: boolean
} | null
views: number
stars: number
status: 'pending' | 'approved' | 'rejected'
tags: string[]
requiredCredentials: unknown
state: unknown
createdAt: Date | string
updatedAt: Date | string
isStarred?: boolean
}>
| undefined
if (session?.user?.id) {
const whereCondition = effectiveSuperUser ? undefined : eq(templates.status, 'approved')
rows = await db
.select({
id: templates.id,
workflowId: templates.workflowId,
name: templates.name,
details: templates.details,
creatorId: templates.creatorId,
creator: templateCreators,
views: templates.views,
stars: templates.stars,
status: templates.status,
tags: templates.tags,
requiredCredentials: templates.requiredCredentials,
state: templates.state,
createdAt: templates.createdAt,
updatedAt: templates.updatedAt,
isStarred: sql<boolean>`CASE WHEN ${templateStars.id} IS NOT NULL THEN true ELSE false END`,
})
.from(templates)
.leftJoin(
templateStars,
and(eq(templateStars.templateId, templates.id), eq(templateStars.userId, session.user.id))
)
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
.where(whereCondition)
.orderBy(desc(templates.views), desc(templates.createdAt))
} else {
rows = await db
.select({
id: templates.id,
workflowId: templates.workflowId,
name: templates.name,
details: templates.details,
creatorId: templates.creatorId,
creator: templateCreators,
views: templates.views,
stars: templates.stars,
status: templates.status,
tags: templates.tags,
requiredCredentials: templates.requiredCredentials,
state: templates.state,
createdAt: templates.createdAt,
updatedAt: templates.updatedAt,
})
.from(templates)
.leftJoin(templateCreators, eq(templates.creatorId, templateCreators.id))
.where(eq(templates.status, 'approved'))
.orderBy(desc(templates.views), desc(templates.createdAt))
.then((r) => r.map((row) => ({ ...row, isStarred: false })))
}
const initialTemplates: WorkspaceTemplate[] =
rows?.map((row) => {
const authorType = (row.creator?.referenceType as 'user' | 'organization') ?? 'user'
const organizationId =
row.creator?.referenceType === 'organization' ? row.creator.referenceId : null
const userId =
row.creator?.referenceType === 'user' ? row.creator.referenceId : '' /* no owner context */
return {
// New structure fields
id: row.id,
workflowId: row.workflowId,
name: row.name,
details: row.details as { tagline?: string; about?: string } | null,
creatorId: row.creatorId,
creator: row.creator
? {
id: row.creator.id,
name: row.creator.name,
profileImageUrl: row.creator.profileImageUrl,
details: row.creator.details as {
about?: string
xUrl?: string
linkedinUrl?: string
websiteUrl?: string
contactEmail?: string
} | null,
referenceType: row.creator.referenceType,
referenceId: row.creator.referenceId,
verified: row.creator.verified,
}
: null,
views: row.views,
stars: row.stars,
status: row.status,
tags: row.tags,
requiredCredentials: row.requiredCredentials,
state: row.state as WorkspaceTemplate['state'],
createdAt: row.createdAt,
updatedAt: row.updatedAt,
isStarred: row.isStarred ?? false,
isSuperUser: effectiveSuperUser,
// Legacy fields for backward compatibility
userId,
description: (row.details as any)?.tagline ?? null,
author: row.creator?.name ?? 'Unknown',
authorType,
organizationId,
color: '#3972F6', // default color for workspace cards
icon: 'Workflow', // default icon for workspace cards
}
}) ?? []
return (
<Templates
initialTemplates={initialTemplates}
currentUserId={session?.user?.id || ''}
isSuperUser={effectiveSuperUser}
/>
)
export default function TemplatesPage() {
notFound()
}
@@ -9,7 +9,7 @@ import type { GlobalCommand } from '@/app/workspace/[workspaceId]/providers/glob
export type CommandId =
| 'accept-diff-changes'
| 'add-agent'
| 'goto-templates'
// | 'goto-templates'
| 'goto-logs'
| 'open-search'
| 'run-workflow'
@@ -52,11 +52,11 @@ export const COMMAND_DEFINITIONS: Record<CommandId, CommandDefinition> = {
shortcut: 'Mod+Shift+A',
allowInEditable: true,
},
'goto-templates': {
id: 'goto-templates',
shortcut: 'Mod+Y',
allowInEditable: true,
},
// 'goto-templates': {
// id: 'goto-templates',
// shortcut: 'Mod+Y',
// allowInEditable: true,
// },
'goto-logs': {
id: 'goto-logs',
shortcut: 'Mod+L',
@@ -2,7 +2,7 @@
import { useCallback } from 'react'
import { createLogger } from '@sim/logger'
import { Layout, Search } from 'lucide-react'
import { Search } from 'lucide-react'
import Image from 'next/image'
import { useParams, useRouter } from 'next/navigation'
import { Button, Library } from '@/components/emcn'
@@ -29,11 +29,11 @@ interface CommandItem {
* Available commands list
*/
const commands: CommandItem[] = [
{
label: 'Templates',
icon: Layout,
shortcut: 'Y',
},
// {
// label: 'Templates',
// icon: Layout,
// shortcut: 'Y',
// },
{
label: 'New Agent',
icon: AgentIcon,
@@ -78,14 +78,14 @@ export function CommandList() {
(label: string) => {
try {
switch (label) {
case 'Templates': {
if (!workspaceId) {
logger.warn('No workspace ID found, cannot navigate to templates from command list')
return
}
router.push(`/workspace/${workspaceId}/templates`)
return
}
// case 'Templates': {
// if (!workspaceId) {
// logger.warn('No workspace ID found, cannot navigate to templates from command list')
// return
// }
// router.push(`/workspace/${workspaceId}/templates`)
// return
// }
case 'New Agent': {
const event = new CustomEvent('add-block-from-toolbar', {
detail: { type: 'agent', enableTriggerMode: false },
@@ -33,7 +33,7 @@ import {
useDeployWorkflow,
useUndeployWorkflow,
} from '@/hooks/queries/deployments'
import { useTemplateByWorkflow } from '@/hooks/queries/templates'
// import { useTemplateByWorkflow } from '@/hooks/queries/templates'
import { useWorkflowMcpServers } from '@/hooks/queries/workflow-mcp-servers'
import { useWorkspaceSettings } from '@/hooks/queries/workspace'
import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -73,7 +73,7 @@ interface WorkflowDeploymentInfoUI {
isPublicApi: boolean
}
type TabView = 'general' | 'api' | 'chat' | 'template' | 'mcp' | 'form' | 'a2a'
type TabView = 'general' | 'api' | 'chat' | /* 'template' | */ 'mcp' | 'form' | 'a2a'
export function DeployModal({
open,
@@ -102,8 +102,8 @@ export function DeployModal({
const [selectedStreamingOutputs, setSelectedStreamingOutputs] = useState<string[]>([])
const [showUndeployConfirm, setShowUndeployConfirm] = useState(false)
const [templateFormValid, setTemplateFormValid] = useState(false)
const [templateSubmitting, setTemplateSubmitting] = useState(false)
// const [templateFormValid, setTemplateFormValid] = useState(false)
// const [templateSubmitting, setTemplateSubmitting] = useState(false)
const [mcpToolSubmitting, setMcpToolSubmitting] = useState(false)
const [mcpToolCanSave, setMcpToolCanSave] = useState(false)
const [a2aSubmitting, setA2aSubmitting] = useState(false)
@@ -159,17 +159,17 @@ export function DeployModal({
const hasA2aAgent = !!existingA2aAgent
const isA2aPublished = existingA2aAgent?.isPublished ?? false
const { data: existingTemplate } = useTemplateByWorkflow(workflowId || '', {
enabled: !!workflowId,
})
const hasExistingTemplate = !!existingTemplate
const templateStatus = existingTemplate
? {
status: existingTemplate.status as 'pending' | 'approved' | 'rejected' | null,
views: existingTemplate.views,
stars: existingTemplate.stars,
}
: null
// const { data: existingTemplate } = useTemplateByWorkflow(workflowId || '', {
// enabled: !!workflowId,
// })
// const hasExistingTemplate = !!existingTemplate
// const templateStatus = existingTemplate
// ? {
// status: existingTemplate.status as 'pending' | 'approved' | 'rejected' | null,
// views: existingTemplate.views,
// stars: existingTemplate.stars,
// }
// : null
const deployMutation = useDeployWorkflow()
const undeployMutation = useUndeployWorkflow()
@@ -406,10 +406,10 @@ export function DeployModal({
}
}, [])
const handleTemplateFormSubmit = useCallback(() => {
const form = document.getElementById('template-deploy-form') as HTMLFormElement
form?.requestSubmit()
}, [])
// const handleTemplateFormSubmit = useCallback(() => {
// const form = document.getElementById('template-deploy-form') as HTMLFormElement
// form?.requestSubmit()
// }, [])
const handleMcpToolFormSubmit = useCallback(() => {
const form = document.getElementById('mcp-deploy-form') as HTMLFormElement
@@ -453,11 +453,11 @@ export function DeployModal({
setShowA2aDeleteConfirm(false)
}, [])
const handleTemplateDelete = useCallback(() => {
const form = document.getElementById('template-deploy-form')
const deleteTrigger = form?.querySelector('[data-template-delete-trigger]') as HTMLButtonElement
deleteTrigger?.click()
}, [])
// const handleTemplateDelete = useCallback(() => {
// const form = document.getElementById('template-deploy-form')
// const deleteTrigger = form?.querySelector('[data-template-delete-trigger]') as HTMLButtonElement
// deleteTrigger?.click()
// }, [])
const isSubmitting = deployMutation.isPending
const isUndeploying = undeployMutation.isPending
@@ -938,28 +938,28 @@ function StatusBadge({ isWarning }: StatusBadgeProps) {
)
}
interface TemplateStatusBadgeProps {
status: 'pending' | 'approved' | 'rejected' | null
views?: number
stars?: number
}
// interface TemplateStatusBadgeProps {
// status: 'pending' | 'approved' | 'rejected' | null
// views?: number
// stars?: number
// }
function TemplateStatusBadge({ status, views, stars }: TemplateStatusBadgeProps) {
const isPending = status === 'pending'
const label = isPending ? 'Under review' : 'Live'
// function TemplateStatusBadge({ status, views, stars }: TemplateStatusBadgeProps) {
// const isPending = status === 'pending'
// const label = isPending ? 'Under review' : 'Live'
const statsText =
status === 'approved' && views !== undefined && views > 0
? `${views} views${stars !== undefined && stars > 0 ? ` • ${stars} stars` : ''}`
: null
// const statsText =
// status === 'approved' && views !== undefined && views > 0
// ? `${views} views${stars !== undefined && stars > 0 ? ` • ${stars} stars` : ''}`
// : null
return (
<Badge variant={isPending ? 'amber' : 'green'} size='lg' dot>
{label}
{statsText && <span>• {statsText}</span>}
</Badge>
)
}
// return (
// <Badge variant={isPending ? 'amber' : 'green'} size='lg' dot>
// {label}
// {statsText && <span>• {statsText}</span>}
// </Badge>
// )
// }
interface GeneralFooterProps {
isDeployed?: boolean
@@ -1,7 +1,7 @@
import { useCallback, useEffect, useMemo, useRef } from 'react'
import { createLogger } from '@sim/logger'
import { useQueryClient } from '@tanstack/react-query'
import { usePathname, useRouter } from 'next/navigation'
import { useRouter } from 'next/navigation'
import { useLeaveWorkspace } from '@/hooks/queries/invitations'
import {
useCreateWorkspace,
@@ -33,7 +33,6 @@ export function useWorkspaceManagement({
sessionUserId,
}: UseWorkspaceManagementProps) {
const router = useRouter()
const pathname = usePathname()
const queryClient = useQueryClient()
const switchToWorkspace = useWorkflowRegistry((state) => state.switchToWorkspace)
@@ -50,12 +49,10 @@ export function useWorkspaceManagement({
const workspaceIdRef = useRef<string>(workspaceId)
const routerRef = useRef<ReturnType<typeof useRouter>>(router)
const pathnameRef = useRef<string | null>(pathname || null)
const hasValidatedRef = useRef<boolean>(false)
workspaceIdRef.current = workspaceId
routerRef.current = router
pathnameRef.current = pathname || null
const activeWorkspace = useMemo(() => {
if (!workspaces.length) return null
@@ -114,16 +111,7 @@ export function useWorkspaceManagement({
try {
await switchToWorkspace(workspace.id)
const currentPath = pathnameRef.current || ''
const templateDetailMatch = currentPath.match(/^\/workspace\/[^/]+\/templates\/([^/]+)$/)
if (templateDetailMatch) {
const templateId = templateDetailMatch[1]
routerRef.current?.push(`/workspace/${workspace.id}/templates/${templateId}`)
} else if (/^\/workspace\/[^/]+\/templates$/.test(currentPath)) {
routerRef.current?.push(`/workspace/${workspace.id}/templates`)
} else {
routerRef.current?.push(`/workspace/${workspace.id}/home`)
}
routerRef.current?.push(`/workspace/${workspace.id}/home`)
logger.info(`Switched to workspace: ${workspace.name} (${workspace.id})`)
} catch (error) {
logger.error('Error switching workspace:', error)
@@ -919,22 +919,22 @@ export const Sidebar = memo(function Sidebar() {
}
},
},
{
id: 'goto-templates',
handler: () => {
try {
const pathWorkspaceId = resolveWorkspaceIdFromPath()
if (pathWorkspaceId) {
navigateToPage(`/workspace/${pathWorkspaceId}/templates`)
logger.info('Navigated to templates', { workspaceId: pathWorkspaceId })
} else {
logger.warn('No workspace ID found, cannot navigate to templates')
}
} catch (err) {
logger.error('Failed to navigate to templates', { err })
}
},
},
// {
// id: 'goto-templates',
// handler: () => {
// try {
// const pathWorkspaceId = resolveWorkspaceIdFromPath()
// if (pathWorkspaceId) {
// navigateToPage(`/workspace/${pathWorkspaceId}/templates`)
// logger.info('Navigated to templates', { workspaceId: pathWorkspaceId })
// } else {
// logger.warn('No workspace ID found, cannot navigate to templates')
// }
// } catch (err) {
// logger.error('Failed to navigate to templates', { err })
// }
// },
// },
{
id: 'goto-logs',
handler: () => {