Skip to content

Commit 304fb28

Browse files
improvement(templates): add share button, serve public templates routes for unauthenticated users and workspace one for authenticated users, improve settings style and organization (#1962)
* improvement(templates): add share button, serve public templates routes for unauthenticated users and workspace one for authenticated users, improve settings style and organization * fix lint --------- Co-authored-by: Vikhyath Mondreti <[email protected]>
1 parent 07e803c commit 304fb28

File tree

28 files changed

+1134
-1218
lines changed

28 files changed

+1134
-1218
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { db } from '@sim/db'
2+
import { permissions, workspace } from '@sim/db/schema'
3+
import { and, desc, eq } from 'drizzle-orm'
4+
import { redirect } from 'next/navigation'
5+
import { getSession } from '@/lib/auth'
6+
7+
export const dynamic = 'force-dynamic'
8+
export const revalidate = 0
9+
10+
interface TemplateLayoutProps {
11+
children: React.ReactNode
12+
params: Promise<{
13+
id: string
14+
}>
15+
}
16+
17+
/**
18+
* Template detail layout (public scope).
19+
* - If user is authenticated, redirect to workspace-scoped template detail.
20+
* - Otherwise render the public template detail children.
21+
*/
22+
export default async function TemplateDetailLayout({ children, params }: TemplateLayoutProps) {
23+
const { id } = await params
24+
const session = await getSession()
25+
26+
if (session?.user?.id) {
27+
const userWorkspaces = await db
28+
.select({
29+
workspace: workspace,
30+
})
31+
.from(permissions)
32+
.innerJoin(workspace, eq(permissions.entityId, workspace.id))
33+
.where(and(eq(permissions.userId, session.user.id), eq(permissions.entityType, 'workspace')))
34+
.orderBy(desc(workspace.createdAt))
35+
.limit(1)
36+
37+
if (userWorkspaces.length > 0) {
38+
const firstWorkspace = userWorkspaces[0].workspace
39+
redirect(`/workspace/${firstWorkspace.id}/templates/${id}`)
40+
}
41+
}
42+
43+
return children
44+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
import TemplateDetails from './template'
22

3+
/**
4+
* Public template detail page for unauthenticated users.
5+
* Authenticated-user redirect is handled in templates/[id]/layout.tsx.
6+
*/
37
export default function TemplatePage() {
48
return <TemplateDetails />
59
}

apps/sim/app/templates/[id]/template.tsx

Lines changed: 95 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,20 @@ import {
99
Globe,
1010
Linkedin,
1111
Mail,
12+
Share2,
1213
Star,
1314
User,
1415
} from 'lucide-react'
1516
import { useParams, useRouter, useSearchParams } from 'next/navigation'
1617
import ReactMarkdown from 'react-markdown'
17-
import { Button } from '@/components/emcn'
18+
import {
19+
Button,
20+
Copy,
21+
Popover,
22+
PopoverContent,
23+
PopoverItem,
24+
PopoverTrigger,
25+
} from '@/components/emcn'
1826
import {
1927
DropdownMenu,
2028
DropdownMenuContent,
@@ -23,6 +31,7 @@ import {
2331
} from '@/components/ui/dropdown-menu'
2432
import { useSession } from '@/lib/auth-client'
2533
import { createLogger } from '@/lib/logs/console/logger'
34+
import { getBaseUrl } from '@/lib/urls/utils'
2635
import { cn } from '@/lib/utils'
2736
import type { CredentialRequirement } from '@/lib/workflows/credential-extractor'
2837
import type { Template } from '@/app/templates/templates'
@@ -63,7 +72,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
6372
>([])
6473
const [isLoadingWorkspaces, setIsLoadingWorkspaces] = useState(false)
6574
const [showWorkspaceSelectorForEdit, setShowWorkspaceSelectorForEdit] = useState(false)
66-
const [showWorkspaceSelectorForUse, setShowWorkspaceSelectorForUse] = useState(false)
75+
const [sharePopoverOpen, setSharePopoverOpen] = useState(false)
6776

6877
const currentUserId = session?.user?.id || null
6978

@@ -351,8 +360,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
351360
// In workspace context, use current workspace directly
352361
if (isWorkspaceContext && workspaceId) {
353362
handleWorkspaceSelectForUse(workspaceId)
354-
} else {
355-
setShowWorkspaceSelectorForUse(true)
356363
}
357364
}
358365

@@ -415,7 +422,6 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
415422
if (isUsing || !template) return
416423

417424
setIsUsing(true)
418-
setShowWorkspaceSelectorForUse(false)
419425
try {
420426
const response = await fetch(`/api/templates/${template.id}/use`, {
421427
method: 'POST',
@@ -518,6 +524,57 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
518524
}
519525
}
520526

527+
/**
528+
* Shares the template to X (Twitter)
529+
*/
530+
const handleShareToTwitter = () => {
531+
if (!template) return
532+
533+
setSharePopoverOpen(false)
534+
const templateUrl = `${getBaseUrl()}/templates/${template.id}`
535+
536+
let tweetText = `🚀 Check out this workflow template: ${template.name}`
537+
538+
if (template.details?.tagline) {
539+
const taglinePreview =
540+
template.details.tagline.length > 100
541+
? `${template.details.tagline.substring(0, 100)}...`
542+
: template.details.tagline
543+
tweetText += `\n\n${taglinePreview}`
544+
}
545+
546+
const maxTextLength = 280 - 23 - 1
547+
if (tweetText.length > maxTextLength) {
548+
tweetText = `${tweetText.substring(0, maxTextLength - 3)}...`
549+
}
550+
551+
const twitterUrl = `https://twitter.com/intent/tweet?text=${encodeURIComponent(tweetText)}&url=${encodeURIComponent(templateUrl)}`
552+
window.open(twitterUrl, '_blank', 'noopener,noreferrer')
553+
}
554+
555+
/**
556+
* Shares the template to LinkedIn.
557+
*/
558+
const handleShareToLinkedIn = () => {
559+
if (!template) return
560+
561+
setSharePopoverOpen(false)
562+
const templateUrl = `${getBaseUrl()}/templates/${template.id}`
563+
const linkedInUrl = `https://www.linkedin.com/sharing/share-offsite/?url=${encodeURIComponent(templateUrl)}`
564+
window.open(linkedInUrl, '_blank', 'noopener,noreferrer')
565+
}
566+
567+
const handleCopyLink = async () => {
568+
setSharePopoverOpen(false)
569+
const templateUrl = `${getBaseUrl()}/templates/${template?.id}`
570+
try {
571+
await navigator.clipboard.writeText(templateUrl)
572+
logger.info('Template link copied to clipboard')
573+
} catch (error) {
574+
logger.error('Failed to copy link:', error)
575+
}
576+
}
577+
521578
return (
522579
<div className={cn('flex min-h-screen flex-col', isWorkspaceContext && 'pl-64')}>
523580
<div className='flex flex-1 overflow-hidden'>
@@ -530,7 +587,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
530587
className='flex items-center gap-[6px] font-medium text-[#ADADAD] text-[14px] transition-colors hover:text-white'
531588
>
532589
<ArrowLeft className='h-[14px] w-[14px]' />
533-
<span>Back</span>
590+
<span>More Templates</span>
534591
</button>
535592
</div>
536593

@@ -622,7 +679,7 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
622679
<>
623680
{!currentUserId ? (
624681
<Button
625-
variant='active'
682+
variant='primary'
626683
onClick={() => {
627684
const callbackUrl =
628685
isWorkspaceContext && workspaceId
@@ -645,48 +702,39 @@ export default function TemplateDetails({ isWorkspaceContext = false }: Template
645702
>
646703
{isUsing ? 'Creating...' : 'Use template'}
647704
</Button>
648-
) : (
649-
<DropdownMenu
650-
open={showWorkspaceSelectorForUse}
651-
onOpenChange={setShowWorkspaceSelectorForUse}
652-
>
653-
<DropdownMenuTrigger asChild>
654-
<Button
655-
variant='primary'
656-
onClick={() => setShowWorkspaceSelectorForUse(true)}
657-
disabled={isUsing || isLoadingWorkspaces}
658-
className='h-[32px] rounded-[6px] px-[16px] text-[#FFFFFF] text-[14px]'
659-
>
660-
{isUsing ? 'Creating...' : isLoadingWorkspaces ? 'Loading...' : 'Use'}
661-
<ChevronDown className='ml-2 h-4 w-4' />
662-
</Button>
663-
</DropdownMenuTrigger>
664-
<DropdownMenuContent align='end' className='w-56'>
665-
{workspaces.length === 0 ? (
666-
<DropdownMenuItem disabled className='text-muted-foreground text-sm'>
667-
No workspaces with write access
668-
</DropdownMenuItem>
669-
) : (
670-
workspaces.map((workspace) => (
671-
<DropdownMenuItem
672-
key={workspace.id}
673-
onClick={() => handleWorkspaceSelectForUse(workspace.id)}
674-
className='flex cursor-pointer items-center justify-between'
675-
>
676-
<div className='flex flex-col'>
677-
<span className='font-medium text-sm'>{workspace.name}</span>
678-
<span className='text-muted-foreground text-xs capitalize'>
679-
{workspace.permissions} access
680-
</span>
681-
</div>
682-
</DropdownMenuItem>
683-
))
684-
)}
685-
</DropdownMenuContent>
686-
</DropdownMenu>
687-
)}
705+
) : null}
688706
</>
689707
)}
708+
709+
{/* Share button */}
710+
<Popover open={sharePopoverOpen} onOpenChange={setSharePopoverOpen}>
711+
<PopoverTrigger asChild>
712+
<Button variant='active' className='h-[32px] rounded-[6px] px-[12px]'>
713+
<Share2 className='h-[14px] w-[14px]' />
714+
</Button>
715+
</PopoverTrigger>
716+
<PopoverContent align='end' side='bottom' sideOffset={8}>
717+
<PopoverItem onClick={handleCopyLink}>
718+
<Copy className='h-3 w-3' />
719+
<span>Copy link</span>
720+
</PopoverItem>
721+
<PopoverItem onClick={handleShareToTwitter}>
722+
<svg
723+
className='h-3 w-3'
724+
viewBox='0 0 24 24'
725+
fill='currentColor'
726+
xmlns='http://www.w3.org/2000/svg'
727+
>
728+
<path d='M18.244 2.25h3.308l-7.227 8.26 8.502 11.24H16.17l-5.214-6.817L4.99 21.75H1.68l7.73-8.835L1.254 2.25H8.08l4.713 6.231zm-1.161 17.52h1.833L7.084 4.126H5.117z' />
729+
</svg>
730+
<span>Share on X</span>
731+
</PopoverItem>
732+
<PopoverItem onClick={handleShareToLinkedIn}>
733+
<Linkedin className='h-3 w-3' />
734+
<span>Share on LinkedIn</span>
735+
</PopoverItem>
736+
</PopoverContent>
737+
</Popover>
690738
</div>
691739
</div>
692740

apps/sim/app/templates/components/navigation-tabs.tsx

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)