+
{template.views} views
@@ -355,9 +386,9 @@ export default function TemplateDetails({
- by {template.author}
+ by {templateAuthor}
- {template.authorType === 'organization' && (
+ {templateAuthorType === 'organization' && (
Organization
@@ -407,16 +438,128 @@ export default function TemplateDetails({
- {/* Divider */}
- {/* Workflow preview */}
-
+ {/* Creator Profile */}
+ {template.creator && (
+
+
Creator
+
+
+
+ {template.creator.profileImageUrl ? (
+
+
+
+ ) : (
+
+
+
+ )}
+
+
+
+
{template.creator.name}
+ {template.creator.details?.about && (
+
+ {template.creator.details.about}
+
+ )}
+
+ {(template.creator.details?.xUrl ||
+ template.creator.details?.linkedinUrl ||
+ template.creator.details?.websiteUrl ||
+ template.creator.details?.contactEmail) && (
+
+ {template.creator.details.xUrl && (
+
+
+ X
+
+ )}
+ {template.creator.details.linkedinUrl && (
+
+
+ LinkedIn
+
+ )}
+ {template.creator.details.websiteUrl && (
+
+
+ Website
+
+ )}
+ {template.creator.details.contactEmail && (
+
+
+ Contact
+
+ )}
+
+ )}
+
+
+
+
+ )}
+
+ {/* Description */}
+ {template.details?.about && (
+
+
Description
+
+ {template.details.about}
+
+
+ )}
+
+
Workflow Preview
-
+
{renderWorkflowPreview()}
+
+ {/* Required Credentials */}
+ {Array.isArray(template.requiredCredentials) && template.requiredCredentials.length > 0 && (
+
+
Credentials Needed
+
+ {template.requiredCredentials.map((cred: CredentialRequirement, idx: number) => {
+ const blockName =
+ getBlock(cred.blockType)?.name ||
+ cred.blockType.charAt(0).toUpperCase() + cred.blockType.slice(1)
+ const alreadyHasBlock = cred.label
+ .toLowerCase()
+ .includes(` for ${blockName.toLowerCase()}`)
+ const text = alreadyHasBlock ? cred.label : `${cred.label} for ${blockName}`
+ return {text}
+ })}
+
+
+ )}
)
diff --git a/apps/sim/app/workspace/[workspaceId]/templates/page.tsx b/apps/sim/app/workspace/[workspaceId]/templates/page.tsx
index e2a59b8790..b8d2b49cd5 100644
--- a/apps/sim/app/workspace/[workspaceId]/templates/page.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/templates/page.tsx
@@ -34,13 +34,13 @@ export default async function TemplatesPage() {
effectiveSuperUser = isSuperUser && superUserModeEnabled
}
- // Load templates (same logic as global page)
+ // Load templates from database
let rows:
| Array<{
id: string
workflowId: string | null
name: string
- details?: any
+ details?: unknown
creatorId: string | null
creator: {
id: string
@@ -124,24 +124,46 @@ export default async function TemplatesPage() {
row.creator?.referenceType === 'user' ? row.creator.referenceId : '' /* no owner context */
return {
+ // New structure fields
id: row.id,
workflowId: row.workflowId,
- userId,
name: row.name,
- description: row.details?.tagline ?? null,
- author: row.creator?.name ?? 'Unknown',
- authorType,
- organizationId,
+ 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,
+ }
+ : null,
views: row.views,
stars: row.stars,
- color: '#3972F6', // default color for workspace cards
- icon: 'Workflow', // default icon for workspace cards
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
}
}) ?? []
diff --git a/apps/sim/app/workspace/[workspaceId]/templates/templates.tsx b/apps/sim/app/workspace/[workspaceId]/templates/templates.tsx
index def9f59e66..889b9237da 100644
--- a/apps/sim/app/workspace/[workspaceId]/templates/templates.tsx
+++ b/apps/sim/app/workspace/[workspaceId]/templates/templates.tsx
@@ -2,37 +2,55 @@
import { useState } from 'react'
import { Layout, Search } from 'lucide-react'
-import { Button } from '@/components/emcn'
-import { Input } from '@/components/ui/input'
+import { Button, Input } from '@/components/emcn'
import { createLogger } from '@/lib/logs/console/logger'
import {
TemplateCard,
TemplateCardSkeleton,
} from '@/app/workspace/[workspaceId]/templates/components/template-card'
import type { WorkflowState } from '@/stores/workflows/workflow/types'
+import type { CreatorProfileDetails } from '@/types/creator-profile'
const logger = createLogger('TemplatesPage')
-// Template data structure
+/**
+ * Template data structure with support for both new and legacy fields
+ */
export interface Template {
id: string
workflowId: string | null
- userId: string
name: string
- description: string | null
- author: string
- authorType: 'user' | 'organization'
- organizationId: string | null
+ details?: {
+ tagline?: string
+ about?: string
+ } | null
+ creatorId: string | null
+ creator?: {
+ id: string
+ name: string
+ profileImageUrl?: string | null
+ details?: CreatorProfileDetails | null
+ referenceType: 'user' | 'organization'
+ referenceId: string
+ } | null
views: number
stars: number
- color: string
- icon: string
status: 'pending' | 'approved' | 'rejected'
+ tags: string[]
+ requiredCredentials: unknown
state: WorkflowState
createdAt: Date | string
updatedAt: Date | string
isStarred: boolean
isSuperUser?: boolean
+ // Legacy fields for backward compatibility with existing UI
+ userId?: string
+ description?: string | null
+ author?: string
+ authorType?: 'user' | 'organization'
+ organizationId?: string | null
+ color?: string
+ icon?: string
}
interface TemplatesProps {
@@ -41,21 +59,23 @@ interface TemplatesProps {
isSuperUser: boolean
}
+/**
+ * Templates list component displaying workflow templates
+ * Supports filtering by tab (gallery/your/pending) and search
+ */
export default function Templates({
initialTemplates,
currentUserId,
isSuperUser,
}: TemplatesProps) {
const [searchQuery, setSearchQuery] = useState('')
- const [activeTab, setActiveTab] = useState('your')
+ const [activeTab, setActiveTab] = useState('gallery')
const [templates, setTemplates] = useState
(initialTemplates)
const [loading, setLoading] = useState(false)
- const handleTabClick = (tabId: string) => {
- setActiveTab(tabId)
- }
-
- // Handle star change callback from template card
+ /**
+ * Update star status for a template
+ */
const handleStarChange = (templateId: string, isStarred: boolean, newStarCount: number) => {
setTemplates((prevTemplates) =>
prevTemplates.map((template) =>
@@ -64,111 +84,90 @@ export default function Templates({
)
}
- // Get templates for the active tab with search filtering
- const getActiveTabTemplates = () => {
- let filtered = templates
+ /**
+ * Filter templates based on active tab and search query
+ */
+ const getFilteredTemplates = () => {
+ const query = searchQuery.toLowerCase()
+
+ return templates.filter((template) => {
+ // Filter by tab
+ const tabMatch =
+ activeTab === 'your'
+ ? template.userId === currentUserId || template.isStarred
+ : activeTab === 'gallery'
+ ? template.status === 'approved'
+ : template.status === 'pending'
+
+ if (!tabMatch) return false
+
+ // Filter by search query
+ if (!query) return true
+
+ const searchableText = [
+ template.name,
+ template.description,
+ template.details?.tagline,
+ template.author,
+ template.creator?.name,
+ ]
+ .filter(Boolean)
+ .join(' ')
+ .toLowerCase()
+
+ return searchableText.includes(query)
+ })
+ }
- // Filter by active tab
- if (activeTab === 'your') {
- filtered = filtered.filter(
- (template) => template.userId === currentUserId || template.isStarred === true
- )
- } else if (activeTab === 'gallery') {
- // Show all approved templates
- filtered = filtered.filter((template) => template.status === 'approved')
- } else if (activeTab === 'pending') {
- // Show pending templates for super users
- filtered = filtered.filter((template) => template.status === 'pending')
- }
+ const filteredTemplates = getFilteredTemplates()
- // Apply search filter
+ /**
+ * Get empty state message based on current filters
+ */
+ const getEmptyStateMessage = () => {
if (searchQuery) {
- filtered = filtered.filter(
- (template) =>
- template.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
- template.description?.toLowerCase().includes(searchQuery.toLowerCase()) ||
- template.author.toLowerCase().includes(searchQuery.toLowerCase())
- )
+ return {
+ title: 'No templates found',
+ description: 'Try a different search term',
+ }
}
- return filtered
- }
-
- const activeTemplates = getActiveTabTemplates()
-
- // Helper function to render template cards
- const renderTemplateCard = (template: Template) => (
-
- )
+ const messages = {
+ pending: {
+ title: 'No pending templates',
+ description: 'New submissions will appear here',
+ },
+ your: {
+ title: 'No templates yet',
+ description: 'Create or star templates to see them here',
+ },
+ gallery: {
+ title: 'No templates available',
+ description: 'Templates will appear once approved',
+ },
+ }
- // Render skeleton cards for loading state
- const renderSkeletonCards = () => {
- return Array.from({ length: 8 }).map((_, index) => (
-
- ))
+ return messages[activeTab as keyof typeof messages] || messages.gallery
}
- // Calculate counts for tabs
- const yourTemplatesCount = templates.filter(
- (template) => template.userId === currentUserId || template.isStarred === true
- ).length
- const galleryCount = templates.filter((template) => template.status === 'approved').length
- const pendingCount = templates.filter((template) => template.status === 'pending').length
-
- const navigationTabs = [
- {
- id: 'gallery',
- label: 'Gallery',
- count: galleryCount,
- },
- {
- id: 'your',
- label: 'Your Templates',
- count: yourTemplatesCount,
- },
- ...(isSuperUser
- ? [
- {
- id: 'pending',
- label: 'Pending',
- count: pendingCount,
- },
- ]
- : []),
- ]
+ const emptyState = getEmptyStateMessage()
return (
- {/* Header */}
-
-
+
Grab a template and start building, or make one from scratch.
- {/* Search and Badges */}
@@ -183,52 +182,60 @@ export default function Templates({
handleTabClick('gallery')}
+ onClick={() => setActiveTab('gallery')}
>
Gallery
handleTabClick('your')}
+ onClick={() => setActiveTab('your')}
>
Your Templates
- {/* Divider */}
- {/* Templates Grid - Based on Active Tab */}
{loading ? (
- renderSkeletonCards()
- ) : activeTemplates.length === 0 ? (
-
+ Array.from({ length: 8 }).map((_, index) => (
+
+ ))
+ ) : filteredTemplates.length === 0 ? (
+
-
- {searchQuery
- ? 'No templates found'
- : activeTab === 'pending'
- ? 'No pending templates'
- : activeTab === 'your'
- ? 'No templates yet'
- : 'No templates available'}
+
+ {emptyState.title}
-
- {searchQuery
- ? 'Try a different search term'
- : activeTab === 'pending'
- ? 'New submissions will appear here'
- : activeTab === 'your'
- ? 'Create or star templates to see them here'
- : 'Templates will appear once approved'}
+
+ {emptyState.description}
) : (
- activeTemplates.map((template) => renderTemplateCard(template))
+ filteredTemplates.map((template) => {
+ const author = template.author || template.creator?.name || 'Unknown'
+
+ return (
+
+ )
+ })
)}