mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bee7f256a6 | ||
|
|
5a9306ebb1 |
@@ -19,7 +19,7 @@ When the user asks you to create a block:
|
||||
```typescript
|
||||
import { {ServiceName}Icon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
|
||||
export const {ServiceName}Block: BlockConfig = {
|
||||
@@ -29,8 +29,6 @@ export const {ServiceName}Block: BlockConfig = {
|
||||
longDescription: 'Detailed description for docs',
|
||||
docsLink: 'https://docs.sim.ai/tools/{service}',
|
||||
category: 'tools', // 'tools' | 'blocks' | 'triggers'
|
||||
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
|
||||
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
|
||||
bgColor: '#HEXCOLOR', // Brand color
|
||||
icon: {ServiceName}Icon,
|
||||
|
||||
@@ -631,7 +629,7 @@ export const registry: Record<string, BlockConfig> = {
|
||||
```typescript
|
||||
import { ServiceIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
|
||||
export const ServiceBlock: BlockConfig = {
|
||||
@@ -641,8 +639,6 @@ export const ServiceBlock: BlockConfig = {
|
||||
longDescription: 'Full description for documentation...',
|
||||
docsLink: 'https://docs.sim.ai/tools/service',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.DeveloperTools,
|
||||
tags: ['oauth', 'api'],
|
||||
bgColor: '#FF6B6B',
|
||||
icon: ServiceIcon,
|
||||
authMode: AuthMode.OAuth,
|
||||
@@ -800,8 +796,6 @@ All tool IDs referenced in `tools.access` and returned by `tools.config.tool` MU
|
||||
|
||||
## Checklist Before Finishing
|
||||
|
||||
- [ ] `integrationType` is set to the correct `IntegrationType` enum value
|
||||
- [ ] `tags` array includes all applicable `IntegrationTag` values
|
||||
- [ ] All subBlocks have `id`, `title` (except switch), and `type`
|
||||
- [ ] Conditions use correct syntax (field, value, not, and)
|
||||
- [ ] DependsOn set for fields that need other values
|
||||
|
||||
@@ -113,7 +113,7 @@ export const {service}{Action}Tool: ToolConfig<Params, Response> = {
|
||||
```typescript
|
||||
import { {Service}Icon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
|
||||
export const {Service}Block: BlockConfig = {
|
||||
@@ -123,8 +123,6 @@ export const {Service}Block: BlockConfig = {
|
||||
longDescription: '...',
|
||||
docsLink: 'https://docs.sim.ai/tools/{service}',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.X, // Primary category (see IntegrationType enum)
|
||||
tags: ['oauth', 'api'], // Cross-cutting tags (see IntegrationTag type)
|
||||
bgColor: '#HEXCOLOR',
|
||||
icon: {Service}Icon,
|
||||
authMode: AuthMode.OAuth, // or AuthMode.ApiKey
|
||||
@@ -412,8 +410,6 @@ If creating V2 versions (API-aligned outputs):
|
||||
|
||||
### Block
|
||||
- [ ] Created `blocks/blocks/{service}.ts`
|
||||
- [ ] Set `integrationType` to the correct `IntegrationType` enum value
|
||||
- [ ] Set `tags` array with all applicable `IntegrationTag` values
|
||||
- [ ] Defined operation dropdown with all operations
|
||||
- [ ] Added credential field with `requiredScopes: getScopesForService('{service}')`
|
||||
- [ ] Added conditional fields per operation
|
||||
|
||||
@@ -4143,11 +4143,12 @@ export function InfisicalIcon(props: SVGProps<SVGSVGElement>) {
|
||||
<svg {...props} viewBox='20 25 233 132' xmlns='http://www.w3.org/2000/svg'>
|
||||
<path
|
||||
d='m191.6 39.4c-20.3 0-37.15 13.21-52.9 30.61-12.99-16.4-29.8-30.61-51.06-30.61-27.74 0-50.44 23.86-50.44 51.33 0 26.68 21.43 51.8 48.98 51.8 20.55 0 37.07-13.86 51.32-31.81 12.69 16.97 29.1 31.41 53.2 31.41 27.13 0 49.85-22.96 49.85-51.4 0-27.12-20.44-51.33-48.95-51.33zm-104.3 77.94c-14.56 0-25.51-12.84-25.51-26.07 0-13.7 10.95-28.29 25.51-28.29 14.93 0 25.71 11.6 37.6 27.34-11.31 15.21-22.23 27.02-37.6 27.02zm104.4 0.25c-15 0-25.28-11.13-37.97-27.37 12.69-16.4 22.01-27.24 37.59-27.24 14.97 0 24.79 13.25 24.79 27.26 0 13-10.17 27.35-24.41 27.35z'
|
||||
fill='currentColor'
|
||||
fill='black'
|
||||
/>
|
||||
</svg>
|
||||
)
|
||||
}
|
||||
|
||||
export function IntercomIcon(props: SVGProps<SVGSVGElement>) {
|
||||
return (
|
||||
<svg
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function LoginLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Skeleton className='h-[38px] w-[80px] rounded-[4px]' />
|
||||
<div className='mt-[32px] w-full space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<div className='mt-[16px] w-full space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-[24px] h-[1px] w-full rounded-[1px]' />
|
||||
<div className='mt-[24px] flex w-full gap-[12px]'>
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[14px] w-[200px] rounded-[4px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { useMemo, useRef, useState } from 'react'
|
||||
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
@@ -87,9 +86,6 @@ export default function LoginPage({
|
||||
const [password, setPassword] = useState('')
|
||||
const [passwordErrors, setPasswordErrors] = useState<string[]>([])
|
||||
const [showValidationError, setShowValidationError] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
const turnstileRef = useRef<TurnstileInstance>(null)
|
||||
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
|
||||
const buttonClass = useBrandedButtonClass()
|
||||
|
||||
const callbackUrlParam = searchParams?.get('callbackUrl')
|
||||
@@ -119,6 +115,19 @@ export default function LoginPage({
|
||||
: null
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
const handleKeyDown = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Enter' && forgotPasswordOpen) {
|
||||
handleForgotPassword()
|
||||
}
|
||||
}
|
||||
|
||||
window.addEventListener('keydown', handleKeyDown)
|
||||
return () => {
|
||||
window.removeEventListener('keydown', handleKeyDown)
|
||||
}
|
||||
}, [forgotPasswordEmail, forgotPasswordOpen])
|
||||
|
||||
const handleEmailChange = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newEmail = e.target.value
|
||||
setEmail(newEmail)
|
||||
@@ -169,21 +178,6 @@ export default function LoginPage({
|
||||
const safeCallbackUrl = callbackUrl
|
||||
let errorHandled = false
|
||||
|
||||
// Execute Turnstile challenge on submit and get a fresh token
|
||||
let token: string | undefined
|
||||
if (turnstileSiteKey && turnstileRef.current) {
|
||||
try {
|
||||
turnstileRef.current.reset()
|
||||
turnstileRef.current.execute()
|
||||
token = await turnstileRef.current.getResponsePromise(15_000)
|
||||
} catch {
|
||||
setFormError('Captcha verification failed. Please try again.')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setFormError(null)
|
||||
const result = await client.signIn.email(
|
||||
{
|
||||
email,
|
||||
@@ -191,11 +185,6 @@ export default function LoginPage({
|
||||
callbackURL: safeCallbackUrl,
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
...(token ? { 'x-captcha-response': token } : {}),
|
||||
},
|
||||
},
|
||||
onError: (ctx) => {
|
||||
logger.error('Login error:', ctx.error)
|
||||
|
||||
@@ -471,20 +460,6 @@ export default function LoginPage({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{turnstileSiteKey && (
|
||||
<Turnstile
|
||||
ref={turnstileRef}
|
||||
siteKey={turnstileSiteKey}
|
||||
options={{ size: 'invisible', execution: 'execute' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{formError && (
|
||||
<div className='text-red-400 text-xs'>
|
||||
<p>{formError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isLoading}
|
||||
@@ -565,51 +540,45 @@ export default function LoginPage({
|
||||
<ModalContent className='dark' size='sm'>
|
||||
<ModalHeader>Reset Password</ModalHeader>
|
||||
<ModalBody>
|
||||
<form
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
handleForgotPassword()
|
||||
}}
|
||||
>
|
||||
<ModalDescription className='mb-4 text-[var(--text-muted)] text-sm'>
|
||||
Enter your email address and we'll send you a link to reset your password if your
|
||||
account exists.
|
||||
</ModalDescription>
|
||||
<div className='space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='reset-email'>Email</Label>
|
||||
<Input
|
||||
id='reset-email'
|
||||
value={forgotPasswordEmail}
|
||||
onChange={(e) => setForgotPasswordEmail(e.target.value)}
|
||||
placeholder='Enter your email'
|
||||
required
|
||||
type='email'
|
||||
className={cn(
|
||||
resetStatus.type === 'error' && 'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
/>
|
||||
{resetStatus.type === 'error' && (
|
||||
<div className='mt-1 text-red-400 text-xs'>
|
||||
<p>{resetStatus.message}</p>
|
||||
</div>
|
||||
<ModalDescription className='mb-4 text-[var(--text-muted)] text-sm'>
|
||||
Enter your email address and we'll send you a link to reset your password if your
|
||||
account exists.
|
||||
</ModalDescription>
|
||||
<div className='space-y-4'>
|
||||
<div className='space-y-2'>
|
||||
<Label htmlFor='reset-email'>Email</Label>
|
||||
<Input
|
||||
id='reset-email'
|
||||
value={forgotPasswordEmail}
|
||||
onChange={(e) => setForgotPasswordEmail(e.target.value)}
|
||||
placeholder='Enter your email'
|
||||
required
|
||||
type='email'
|
||||
className={cn(
|
||||
resetStatus.type === 'error' && 'border-red-500 focus:border-red-500'
|
||||
)}
|
||||
</div>
|
||||
{resetStatus.type === 'success' && (
|
||||
<div className='mt-1 text-[#4CAF50] text-xs'>
|
||||
/>
|
||||
{resetStatus.type === 'error' && (
|
||||
<div className='mt-1 text-red-400 text-xs'>
|
||||
<p>{resetStatus.message}</p>
|
||||
</div>
|
||||
)}
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isSubmittingReset}
|
||||
loading={isSubmittingReset}
|
||||
loadingText='Sending'
|
||||
>
|
||||
Send Reset Link
|
||||
</BrandedButton>
|
||||
</div>
|
||||
</form>
|
||||
{resetStatus.type === 'success' && (
|
||||
<div className='mt-1 text-[#4CAF50] text-xs'>
|
||||
<p>{resetStatus.message}</p>
|
||||
</div>
|
||||
)}
|
||||
<BrandedButton
|
||||
type='button'
|
||||
onClick={handleForgotPassword}
|
||||
disabled={isSubmittingReset}
|
||||
loading={isSubmittingReset}
|
||||
loadingText='Sending'
|
||||
>
|
||||
Send Reset Link
|
||||
</BrandedButton>
|
||||
</div>
|
||||
</ModalBody>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function OAuthConsentLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<div className='flex items-center gap-[16px]'>
|
||||
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
|
||||
<Skeleton className='h-[20px] w-[20px] rounded-[4px]' />
|
||||
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[38px] w-[220px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[8px] h-[14px] w-[280px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[24px] h-[56px] w-full rounded-[8px]' />
|
||||
<Skeleton className='mt-[16px] h-[120px] w-full rounded-[8px]' />
|
||||
<div className='mt-[24px] flex w-full max-w-[410px] gap-[12px]'>
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function ResetPasswordLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Skeleton className='h-[38px] w-[160px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[12px] h-[14px] w-[280px] rounded-[4px]' />
|
||||
<div className='mt-[32px] w-full space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-[24px] h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function SignupLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Skeleton className='h-[38px] w-[100px] rounded-[4px]' />
|
||||
<div className='mt-[32px] w-full space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<div className='mt-[16px] w-full space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[40px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<div className='mt-[16px] w-full space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[64px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-[24px] h-[1px] w-full rounded-[1px]' />
|
||||
<div className='mt-[24px] flex w-full gap-[12px]'>
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
<Skeleton className='h-[44px] flex-1 rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[14px] w-[220px] rounded-[4px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
'use client'
|
||||
|
||||
import { Suspense, useMemo, useRef, useState } from 'react'
|
||||
import { Turnstile, type TurnstileInstance } from '@marsidev/react-turnstile'
|
||||
import { Suspense, useMemo, useState } from 'react'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { Eye, EyeOff } from 'lucide-react'
|
||||
import Link from 'next/link'
|
||||
@@ -91,9 +90,6 @@ function SignupFormContent({
|
||||
const [emailError, setEmailError] = useState('')
|
||||
const [emailErrors, setEmailErrors] = useState<string[]>([])
|
||||
const [showEmailValidationError, setShowEmailValidationError] = useState(false)
|
||||
const [formError, setFormError] = useState<string | null>(null)
|
||||
const turnstileRef = useRef<TurnstileInstance>(null)
|
||||
const turnstileSiteKey = useMemo(() => getEnv('NEXT_PUBLIC_TURNSTILE_SITE_KEY'), [])
|
||||
const buttonClass = useBrandedButtonClass()
|
||||
|
||||
const redirectUrl = useMemo(
|
||||
@@ -249,21 +245,6 @@ function SignupFormContent({
|
||||
|
||||
const sanitizedName = trimmedName
|
||||
|
||||
// Execute Turnstile challenge on submit and get a fresh token
|
||||
let token: string | undefined
|
||||
if (turnstileSiteKey && turnstileRef.current) {
|
||||
try {
|
||||
turnstileRef.current.reset()
|
||||
turnstileRef.current.execute()
|
||||
token = await turnstileRef.current.getResponsePromise(15_000)
|
||||
} catch {
|
||||
setFormError('Captcha verification failed. Please try again.')
|
||||
setIsLoading(false)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
setFormError(null)
|
||||
const response = await client.signUp.email(
|
||||
{
|
||||
email: emailValue,
|
||||
@@ -271,11 +252,6 @@ function SignupFormContent({
|
||||
name: sanitizedName,
|
||||
},
|
||||
{
|
||||
fetchOptions: {
|
||||
headers: {
|
||||
...(token ? { 'x-captcha-response': token } : {}),
|
||||
},
|
||||
},
|
||||
onError: (ctx) => {
|
||||
logger.error('Signup error:', ctx.error)
|
||||
const errorMessage: string[] = ['Failed to create account']
|
||||
@@ -477,20 +453,6 @@ function SignupFormContent({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{turnstileSiteKey && (
|
||||
<Turnstile
|
||||
ref={turnstileRef}
|
||||
siteKey={turnstileSiteKey}
|
||||
options={{ size: 'invisible', execution: 'execute' }}
|
||||
/>
|
||||
)}
|
||||
|
||||
{formError && (
|
||||
<div className='text-red-400 text-xs'>
|
||||
<p>{formError}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<BrandedButton
|
||||
type='submit'
|
||||
disabled={isLoading}
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function SSOLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Skeleton className='h-[38px] w-[120px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[12px] h-[14px] w-[260px] rounded-[4px]' />
|
||||
<div className='mt-[32px] w-full space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[80px] rounded-[4px]' />
|
||||
<Skeleton className='h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[44px] w-full rounded-[10px]' />
|
||||
<Skeleton className='mt-[24px] h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function VerifyLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center'>
|
||||
<Skeleton className='h-[38px] w-[180px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[12px] h-[14px] w-[300px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[4px] h-[14px] w-[240px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[32px] h-[44px] w-full rounded-[10px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -460,7 +460,7 @@ function TrustStrip() {
|
||||
<div className='mx-6 mt-4 grid grid-cols-1 overflow-hidden rounded-[8px] border border-[#2A2A2A] sm:grid-cols-3 md:mx-8'>
|
||||
{/* SOC 2 + HIPAA combined */}
|
||||
<Link
|
||||
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
|
||||
href='https://trust.delve.co/sim-studio'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
className='group flex items-center gap-3 border-[#2A2A2A] border-b px-4 py-[14px] transition-colors hover:bg-[#212121] sm:border-r sm:border-b-0'
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function BlogPostLoading() {
|
||||
return (
|
||||
<article className='w-full'>
|
||||
{/* Header area */}
|
||||
<div className='mx-auto max-w-[1450px] px-6 pt-8 sm:px-8 sm:pt-12 md:px-12 md:pt-16'>
|
||||
{/* Back link */}
|
||||
<div className='mb-6'>
|
||||
<Skeleton className='h-[16px] w-[60px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
{/* Image + title row */}
|
||||
<div className='flex flex-col gap-8 md:flex-row md:gap-12'>
|
||||
{/* Image */}
|
||||
<div className='w-full flex-shrink-0 md:w-[450px]'>
|
||||
<Skeleton className='aspect-[450/360] w-full rounded-lg bg-[#2A2A2A]' />
|
||||
</div>
|
||||
{/* Title + author */}
|
||||
<div className='flex flex-1 flex-col justify-between'>
|
||||
<div>
|
||||
<Skeleton className='h-[48px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='mt-[8px] h-[48px] w-[80%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
<div className='mt-4 flex items-center justify-between'>
|
||||
<div className='flex items-center gap-2'>
|
||||
<Skeleton className='h-[24px] w-[24px] rounded-full bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[16px] w-[100px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
<Skeleton className='h-[32px] w-[32px] rounded-[6px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Divider */}
|
||||
<Skeleton className='mt-8 h-[1px] w-full bg-[#2A2A2A] sm:mt-12' />
|
||||
{/* Date + description */}
|
||||
<div className='flex flex-col gap-6 py-8 sm:flex-row sm:items-start sm:justify-between sm:gap-8 sm:py-10'>
|
||||
<Skeleton className='h-[16px] w-[120px] flex-shrink-0 rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='flex-1 space-y-[8px]'>
|
||||
<Skeleton className='h-[20px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[20px] w-[70%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{/* Article body */}
|
||||
<div className='mx-auto max-w-[900px] px-6 pb-20 sm:px-8 md:px-12'>
|
||||
<div className='space-y-[16px]'>
|
||||
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[16px] w-[95%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[16px] w-[88%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='mt-[24px] h-[24px] w-[200px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[16px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[16px] w-[92%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[16px] w-[85%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
)
|
||||
}
|
||||
@@ -1,25 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_POST_COUNT = 4
|
||||
|
||||
export default function AuthorLoading() {
|
||||
return (
|
||||
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
|
||||
<div className='mb-6 flex items-center gap-3'>
|
||||
<Skeleton className='h-[40px] w-[40px] rounded-full bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[32px] w-[160px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
<div className='grid grid-cols-1 gap-8 sm:grid-cols-2'>
|
||||
{Array.from({ length: SKELETON_POST_COUNT }).map((_, i) => (
|
||||
<div key={i} className='overflow-hidden rounded-lg border border-[#2A2A2A]'>
|
||||
<Skeleton className='h-[160px] w-full rounded-none bg-[#2A2A2A]' />
|
||||
<div className='p-3'>
|
||||
<Skeleton className='mb-1 h-[12px] w-[80px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[14px] w-[200px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_CARD_COUNT = 6
|
||||
|
||||
export default function BlogLoading() {
|
||||
return (
|
||||
<main className='mx-auto max-w-[1200px] px-6 py-12 sm:px-8 md:px-12'>
|
||||
<Skeleton className='h-[48px] w-[100px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='mt-3 h-[18px] w-[420px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='mt-10 grid grid-cols-1 gap-4 md:grid-cols-2 md:gap-6 lg:grid-cols-3'>
|
||||
{Array.from({ length: SKELETON_CARD_COUNT }).map((_, i) => (
|
||||
<div key={i} className='flex flex-col overflow-hidden rounded-xl border border-[#2A2A2A]'>
|
||||
<Skeleton className='aspect-video w-full rounded-none bg-[#2A2A2A]' />
|
||||
<div className='flex flex-1 flex-col p-4'>
|
||||
<Skeleton className='mb-2 h-[12px] w-[80px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='mb-1 h-[20px] w-[85%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='mb-3 h-[14px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[14px] w-[70%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='mt-3 flex items-center gap-2'>
|
||||
<Skeleton className='h-[16px] w-[16px] rounded-full bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[12px] w-[80px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_TAG_COUNT = 12
|
||||
|
||||
export default function TagsLoading() {
|
||||
return (
|
||||
<main className='mx-auto max-w-[900px] px-6 py-10 sm:px-8 md:px-12'>
|
||||
<Skeleton className='mb-6 h-[32px] w-[200px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='flex flex-wrap gap-3'>
|
||||
{Array.from({ length: SKELETON_TAG_COUNT }).map((_, i) => (
|
||||
<Skeleton
|
||||
key={i}
|
||||
className='h-[30px] rounded-full bg-[#2A2A2A]'
|
||||
style={{ width: `${60 + (i % 4) * 24}px` }}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -6,11 +6,7 @@ export default function ComplianceBadges() {
|
||||
return (
|
||||
<div className='mt-[6px] flex items-center gap-[12px]'>
|
||||
{/* SOC2 badge */}
|
||||
<Link
|
||||
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Link href='https://trust.delve.co/sim-studio' target='_blank' rel='noopener noreferrer'>
|
||||
<Image
|
||||
src='/footer/soc2.png'
|
||||
alt='SOC2 Compliant'
|
||||
@@ -22,11 +18,7 @@ export default function ComplianceBadges() {
|
||||
/>
|
||||
</Link>
|
||||
{/* HIPAA badge */}
|
||||
<Link
|
||||
href='https://app.vanta.com/sim.ai/trust/v35ia0jil4l7dteqjgaktn'
|
||||
target='_blank'
|
||||
rel='noopener noreferrer'
|
||||
>
|
||||
<Link href='https://trust.delve.co/sim-studio' target='_blank' rel='noopener noreferrer'>
|
||||
<HIPAABadgeIcon className='h-[54px] w-[54px]' />
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -6,132 +6,54 @@ import { blockTypeToIconMap } from '@/app/(landing)/integrations/data/icon-mappi
|
||||
import type { Integration } from '@/app/(landing)/integrations/data/types'
|
||||
import { IntegrationCard } from './integration-card'
|
||||
|
||||
const CATEGORY_LABELS: Record<string, string> = {
|
||||
ai: 'AI',
|
||||
analytics: 'Analytics',
|
||||
automation: 'Automation',
|
||||
communication: 'Communication',
|
||||
crm: 'CRM',
|
||||
'customer-support': 'Customer Support',
|
||||
databases: 'Databases',
|
||||
design: 'Design',
|
||||
'developer-tools': 'Developer Tools',
|
||||
documents: 'Documents',
|
||||
ecommerce: 'E-commerce',
|
||||
email: 'Email',
|
||||
'file-storage': 'File Storage',
|
||||
hr: 'HR',
|
||||
media: 'Media',
|
||||
productivity: 'Productivity',
|
||||
'sales-intelligence': 'Sales Intelligence',
|
||||
search: 'Search',
|
||||
security: 'Security',
|
||||
social: 'Social',
|
||||
other: 'Other',
|
||||
} as const
|
||||
|
||||
interface IntegrationGridProps {
|
||||
integrations: Integration[]
|
||||
}
|
||||
|
||||
export function IntegrationGrid({ integrations }: IntegrationGridProps) {
|
||||
const [query, setQuery] = useState('')
|
||||
const [activeCategory, setActiveCategory] = useState<string | null>(null)
|
||||
|
||||
const availableCategories = useMemo(() => {
|
||||
const counts = new Map<string, number>()
|
||||
for (const i of integrations) {
|
||||
if (i.integrationType) {
|
||||
counts.set(i.integrationType, (counts.get(i.integrationType) || 0) + 1)
|
||||
}
|
||||
}
|
||||
return Array.from(counts.entries())
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.map(([key]) => key)
|
||||
}, [integrations])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
let results = integrations
|
||||
|
||||
if (activeCategory) {
|
||||
results = results.filter((i) => i.integrationType === activeCategory)
|
||||
}
|
||||
|
||||
const q = query.trim().toLowerCase()
|
||||
if (q) {
|
||||
results = results.filter(
|
||||
(i) =>
|
||||
i.name.toLowerCase().includes(q) ||
|
||||
i.description.toLowerCase().includes(q) ||
|
||||
i.operations.some(
|
||||
(op) => op.name.toLowerCase().includes(q) || op.description.toLowerCase().includes(q)
|
||||
) ||
|
||||
i.triggers.some((t) => t.name.toLowerCase().includes(q))
|
||||
)
|
||||
}
|
||||
|
||||
return results
|
||||
}, [integrations, query, activeCategory])
|
||||
if (!q) return integrations
|
||||
return integrations.filter(
|
||||
(i) =>
|
||||
i.name.toLowerCase().includes(q) ||
|
||||
i.description.toLowerCase().includes(q) ||
|
||||
i.operations.some(
|
||||
(op) => op.name.toLowerCase().includes(q) || op.description.toLowerCase().includes(q)
|
||||
) ||
|
||||
i.triggers.some((t) => t.name.toLowerCase().includes(q))
|
||||
)
|
||||
}, [integrations, query])
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='mb-6 flex flex-col gap-4 sm:flex-row sm:items-center'>
|
||||
<div className='relative max-w-[480px] flex-1'>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
<circle cx={11} cy={11} r={8} />
|
||||
<path d='m21 21-4.35-4.35' />
|
||||
</svg>
|
||||
<Input
|
||||
type='search'
|
||||
placeholder='Search integrations, tools, or triggers…'
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className='pl-9'
|
||||
aria-label='Search integrations'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='mb-8 flex flex-wrap gap-2'>
|
||||
<button
|
||||
type='button'
|
||||
onClick={() => setActiveCategory(null)}
|
||||
className={`rounded-md border px-3 py-1 text-[12px] transition-colors ${
|
||||
activeCategory === null
|
||||
? 'border-[#555] bg-[#333] text-[#ECECEC]'
|
||||
: 'border-[#2A2A2A] bg-transparent text-[#999] hover:border-[#3d3d3d] hover:text-[#ECECEC]'
|
||||
}`}
|
||||
<div className='relative mb-8 max-w-[480px]'>
|
||||
<svg
|
||||
aria-hidden='true'
|
||||
className='-translate-y-1/2 pointer-events-none absolute top-1/2 left-3 h-4 w-4 text-[#555]'
|
||||
fill='none'
|
||||
stroke='currentColor'
|
||||
strokeWidth={2}
|
||||
viewBox='0 0 24 24'
|
||||
>
|
||||
All
|
||||
</button>
|
||||
{availableCategories.map((cat) => (
|
||||
<button
|
||||
key={cat}
|
||||
type='button'
|
||||
onClick={() => setActiveCategory(activeCategory === cat ? null : cat)}
|
||||
className={`rounded-md border px-3 py-1 text-[12px] transition-colors ${
|
||||
activeCategory === cat
|
||||
? 'border-[#555] bg-[#333] text-[#ECECEC]'
|
||||
: 'border-[#2A2A2A] bg-transparent text-[#999] hover:border-[#3d3d3d] hover:text-[#ECECEC]'
|
||||
}`}
|
||||
>
|
||||
{CATEGORY_LABELS[cat] || cat}
|
||||
</button>
|
||||
))}
|
||||
<circle cx={11} cy={11} r={8} />
|
||||
<path d='m21 21-4.35-4.35' />
|
||||
</svg>
|
||||
<Input
|
||||
type='search'
|
||||
placeholder='Search integrations, tools, or triggers…'
|
||||
value={query}
|
||||
onChange={(e) => setQuery(e.target.value)}
|
||||
className='pl-9'
|
||||
aria-label='Search integrations'
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filtered.length === 0 ? (
|
||||
<p className='py-12 text-center text-[#555] text-[15px]'>
|
||||
No integrations found
|
||||
{query ? <> for “{query}”</> : null}
|
||||
{activeCategory ? <> in {CATEGORY_LABELS[activeCategory] || activeCategory}</> : null}
|
||||
No integrations found for “{query}”
|
||||
</p>
|
||||
) : (
|
||||
<div className='grid grid-cols-1 gap-4 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4'>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -34,6 +34,4 @@ export interface Integration {
|
||||
triggerCount: number
|
||||
authType: AuthType
|
||||
category: string
|
||||
integrationType?: string
|
||||
tags?: string[]
|
||||
}
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function PrivacyLoading() {
|
||||
return (
|
||||
<main className='min-h-screen bg-[#1C1C1C] text-[#ECECEC]'>
|
||||
<div className='flex h-[52px] items-center border-[#2A2A2A] border-b px-6'>
|
||||
<Skeleton className='h-[22px] w-[60px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='ml-auto flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[30px] w-[64px] rounded-[5px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[30px] w-[80px] rounded-[5px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-auto max-w-[800px] px-6 pt-[60px] pb-[80px] sm:px-12'>
|
||||
<Skeleton className='mx-auto h-[48px] w-[280px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='mt-12 space-y-8'>
|
||||
<div className='space-y-[10px]'>
|
||||
<Skeleton className='h-[15px] w-[180px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[95%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[88%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
<div className='mt-12 space-y-[10px]'>
|
||||
<Skeleton className='h-[28px] w-[320px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='mt-4 space-y-[10px]'>
|
||||
<Skeleton className='h-[20px] w-[160px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[92%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[85%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-12 space-y-[10px]'>
|
||||
<Skeleton className='h-[28px] w-[260px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[90%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='mt-4 space-y-[8px] pl-6'>
|
||||
<Skeleton className='h-[15px] w-[70%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[60%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[75%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[65%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-12 space-y-[10px]'>
|
||||
<Skeleton className='h-[28px] w-[300px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[95%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[88%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
<div className='mt-12 space-y-[10px]'>
|
||||
<Skeleton className='h-[28px] w-[220px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[93%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[80%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,52 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function TermsLoading() {
|
||||
return (
|
||||
<main className='min-h-screen bg-[#1C1C1C] text-[#ECECEC]'>
|
||||
<div className='flex h-[52px] items-center border-[#2A2A2A] border-b px-6'>
|
||||
<Skeleton className='h-[22px] w-[60px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='ml-auto flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[30px] w-[64px] rounded-[5px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[30px] w-[80px] rounded-[5px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-auto max-w-[800px] px-6 pt-[60px] pb-[80px] sm:px-12'>
|
||||
<Skeleton className='mx-auto h-[48px] w-[320px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='mt-12 space-y-8'>
|
||||
<div className='space-y-[10px]'>
|
||||
<Skeleton className='h-[15px] w-[180px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[95%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[88%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
<div className='mt-12 space-y-[10px]'>
|
||||
<Skeleton className='h-[28px] w-[140px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[92%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[85%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
<div className='mt-12 space-y-[10px]'>
|
||||
<Skeleton className='h-[28px] w-[260px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[90%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[75%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
<div className='mt-12 space-y-[10px]'>
|
||||
<Skeleton className='h-[28px] w-[380px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[95%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[88%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
<div className='mt-12 space-y-[10px]'>
|
||||
<Skeleton className='h-[28px] w-[300px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[93%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[15px] w-[80%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -53,11 +53,6 @@ vi.mock('@/lib/auth/hybrid', () => ({
|
||||
vi.mock('@/lib/core/utils/request', () => ({
|
||||
generateRequestId: vi.fn().mockReturnValue('test-req-id'),
|
||||
}))
|
||||
vi.mock('@/lib/audit/log', () => ({
|
||||
recordAudit: vi.fn(),
|
||||
AuditAction: {},
|
||||
AuditResourceType: {},
|
||||
}))
|
||||
|
||||
import { GET, PATCH } from '@/app/api/knowledge/[id]/connectors/[connectorId]/documents/route'
|
||||
|
||||
@@ -173,16 +168,8 @@ describe('Connector Documents API Route', () => {
|
||||
})
|
||||
|
||||
it('returns success for restore operation', async () => {
|
||||
mockCheckSession.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-1',
|
||||
userName: 'Test',
|
||||
userEmail: 'test@test.com',
|
||||
})
|
||||
mockCheckWriteAccess.mockResolvedValue({
|
||||
hasAccess: true,
|
||||
knowledgeBase: { workspaceId: 'ws-1', name: 'Test KB' },
|
||||
})
|
||||
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
|
||||
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
|
||||
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456' }])
|
||||
mockDbChain.returning.mockResolvedValueOnce([{ id: 'doc-1' }])
|
||||
|
||||
@@ -195,16 +182,8 @@ describe('Connector Documents API Route', () => {
|
||||
})
|
||||
|
||||
it('returns success for exclude operation', async () => {
|
||||
mockCheckSession.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-1',
|
||||
userName: 'Test',
|
||||
userEmail: 'test@test.com',
|
||||
})
|
||||
mockCheckWriteAccess.mockResolvedValue({
|
||||
hasAccess: true,
|
||||
knowledgeBase: { workspaceId: 'ws-1', name: 'Test KB' },
|
||||
})
|
||||
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
|
||||
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
|
||||
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456' }])
|
||||
mockDbChain.returning.mockResolvedValueOnce([{ id: 'doc-2' }, { id: 'doc-3' }])
|
||||
|
||||
|
||||
@@ -4,7 +4,6 @@ import { createLogger } from '@sim/logger'
|
||||
import { and, eq, inArray, isNull } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { checkKnowledgeBaseAccess, checkKnowledgeBaseWriteAccess } from '@/app/api/knowledge/utils'
|
||||
@@ -185,19 +184,6 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
|
||||
logger.info(`[${requestId}] Restored ${updated.length} excluded documents`, { connectorId })
|
||||
|
||||
recordAudit({
|
||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||
actorId: auth.userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.CONNECTOR_DOCUMENT_RESTORED,
|
||||
resourceType: AuditResourceType.CONNECTOR,
|
||||
resourceId: connectorId,
|
||||
description: `Restored ${updated.length} excluded document(s) for knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, documentCount: updated.length },
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { restoredCount: updated.length, documentIds: updated.map((d) => d.id) },
|
||||
@@ -220,19 +206,6 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
|
||||
logger.info(`[${requestId}] Excluded ${updated.length} documents`, { connectorId })
|
||||
|
||||
recordAudit({
|
||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||
actorId: auth.userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.CONNECTOR_DOCUMENT_EXCLUDED,
|
||||
resourceType: AuditResourceType.CONNECTOR,
|
||||
resourceId: connectorId,
|
||||
description: `Excluded ${updated.length} document(s) from knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, documentCount: updated.length },
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({
|
||||
success: true,
|
||||
data: { excludedCount: updated.length, documentIds: updated.map((d) => d.id) },
|
||||
|
||||
@@ -75,11 +75,6 @@ vi.mock('@/lib/knowledge/tags/service', () => ({
|
||||
vi.mock('@/lib/knowledge/documents/service', () => ({
|
||||
deleteDocumentStorageFiles: vi.fn().mockResolvedValue(undefined),
|
||||
}))
|
||||
vi.mock('@/lib/audit/log', () => ({
|
||||
recordAudit: vi.fn(),
|
||||
AuditAction: {},
|
||||
AuditResourceType: {},
|
||||
}))
|
||||
|
||||
import { DELETE, GET, PATCH } from '@/app/api/knowledge/[id]/connectors/[connectorId]/route'
|
||||
|
||||
@@ -188,16 +183,8 @@ describe('Knowledge Connector By ID API Route', () => {
|
||||
})
|
||||
|
||||
it('returns 200 and updates status', async () => {
|
||||
mockCheckSession.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-1',
|
||||
userName: 'Test',
|
||||
userEmail: 'test@test.com',
|
||||
})
|
||||
mockCheckWriteAccess.mockResolvedValue({
|
||||
hasAccess: true,
|
||||
knowledgeBase: { workspaceId: 'ws-1', name: 'Test KB' },
|
||||
})
|
||||
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
|
||||
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
|
||||
|
||||
const updatedConnector = { id: 'conn-456', status: 'paused', syncIntervalMinutes: 120 }
|
||||
mockDbChain.limit.mockResolvedValueOnce([updatedConnector])
|
||||
@@ -223,16 +210,8 @@ describe('Knowledge Connector By ID API Route', () => {
|
||||
})
|
||||
|
||||
it('returns 200 on successful hard-delete', async () => {
|
||||
mockCheckSession.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-1',
|
||||
userName: 'Test',
|
||||
userEmail: 'test@test.com',
|
||||
})
|
||||
mockCheckWriteAccess.mockResolvedValue({
|
||||
hasAccess: true,
|
||||
knowledgeBase: { workspaceId: 'ws-1', name: 'Test KB' },
|
||||
})
|
||||
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
|
||||
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
|
||||
mockDbChain.where
|
||||
.mockReturnValueOnce(mockDbChain)
|
||||
.mockResolvedValueOnce([{ id: 'doc-1', fileUrl: '/api/uploads/test.txt' }])
|
||||
|
||||
@@ -11,7 +11,6 @@ import { and, desc, eq, inArray, isNull, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { decryptApiKey } from '@/lib/api-key/crypto'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { deleteDocumentStorageFiles } from '@/lib/knowledge/documents/service'
|
||||
@@ -234,21 +233,6 @@ export async function PATCH(request: NextRequest, { params }: RouteParams) {
|
||||
.limit(1)
|
||||
|
||||
const { encryptedApiKey: __, ...updatedData } = updated[0]
|
||||
|
||||
recordAudit({
|
||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||
actorId: auth.userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.CONNECTOR_UPDATED,
|
||||
resourceType: AuditResourceType.CONNECTOR,
|
||||
resourceId: connectorId,
|
||||
resourceName: updatedData.connectorType,
|
||||
description: `Updated connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, updatedFields: Object.keys(parsed.data) },
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true, data: updatedData })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error updating connector`, error)
|
||||
@@ -276,7 +260,7 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
}
|
||||
|
||||
const existingConnector = await db
|
||||
.select({ id: knowledgeConnector.id, connectorType: knowledgeConnector.connectorType })
|
||||
.select({ id: knowledgeConnector.id })
|
||||
.from(knowledgeConnector)
|
||||
.where(
|
||||
and(
|
||||
@@ -339,20 +323,6 @@ export async function DELETE(request: NextRequest, { params }: RouteParams) {
|
||||
|
||||
logger.info(`[${requestId}] Hard-deleted connector ${connectorId} and its documents`)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||
actorId: auth.userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.CONNECTOR_DELETED,
|
||||
resourceType: AuditResourceType.CONNECTOR,
|
||||
resourceId: connectorId,
|
||||
resourceName: existingConnector[0].connectorType,
|
||||
description: `Deleted connector from knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, documentsDeleted: connectorDocuments.length },
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error deleting connector`, error)
|
||||
|
||||
@@ -43,11 +43,6 @@ vi.mock('@/lib/core/utils/request', () => ({
|
||||
vi.mock('@/lib/knowledge/connectors/sync-engine', () => ({
|
||||
dispatchSync: mockDispatchSync,
|
||||
}))
|
||||
vi.mock('@/lib/audit/log', () => ({
|
||||
recordAudit: vi.fn(),
|
||||
AuditAction: {},
|
||||
AuditResourceType: {},
|
||||
}))
|
||||
|
||||
import { POST } from '@/app/api/knowledge/[id]/connectors/[connectorId]/sync/route'
|
||||
|
||||
@@ -97,16 +92,8 @@ describe('Connector Manual Sync API Route', () => {
|
||||
})
|
||||
|
||||
it('dispatches sync on valid request', async () => {
|
||||
mockCheckSession.mockResolvedValue({
|
||||
success: true,
|
||||
userId: 'user-1',
|
||||
userName: 'Test',
|
||||
userEmail: 'test@test.com',
|
||||
})
|
||||
mockCheckWriteAccess.mockResolvedValue({
|
||||
hasAccess: true,
|
||||
knowledgeBase: { workspaceId: 'ws-1', name: 'Test KB' },
|
||||
})
|
||||
mockCheckSession.mockResolvedValue({ success: true, userId: 'user-1' })
|
||||
mockCheckWriteAccess.mockResolvedValue({ hasAccess: true })
|
||||
mockDbChain.limit.mockResolvedValueOnce([{ id: 'conn-456', status: 'active' }])
|
||||
|
||||
const req = createMockRequest('POST')
|
||||
|
||||
@@ -3,7 +3,6 @@ import { knowledgeConnector } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { and, eq, isNull } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
|
||||
@@ -55,20 +54,6 @@ export async function POST(request: NextRequest, { params }: RouteParams) {
|
||||
|
||||
logger.info(`[${requestId}] Manual sync triggered for connector ${connectorId}`)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||
actorId: auth.userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.CONNECTOR_SYNCED,
|
||||
resourceType: AuditResourceType.CONNECTOR,
|
||||
resourceId: connectorId,
|
||||
resourceName: connectorRows[0].connectorType,
|
||||
description: `Triggered manual sync for connector on knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId },
|
||||
request,
|
||||
})
|
||||
|
||||
dispatchSync(connectorId, { requestId }).catch((error) => {
|
||||
logger.error(
|
||||
`[${requestId}] Failed to dispatch manual sync for connector ${connectorId}`,
|
||||
|
||||
@@ -5,7 +5,6 @@ import { and, desc, eq, isNull, sql } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { z } from 'zod'
|
||||
import { encryptApiKey } from '@/lib/api-key/crypto'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { dispatchSync } from '@/lib/knowledge/connectors/sync-engine'
|
||||
@@ -227,20 +226,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Created connector ${connectorId} for KB ${knowledgeBaseId}`)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: writeCheck.knowledgeBase.workspaceId,
|
||||
actorId: auth.userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.CONNECTOR_CREATED,
|
||||
resourceType: AuditResourceType.CONNECTOR,
|
||||
resourceId: connectorId,
|
||||
resourceName: connectorType,
|
||||
description: `Created ${connectorType} connector for knowledge base "${writeCheck.knowledgeBase.name}"`,
|
||||
metadata: { knowledgeBaseId, connectorType, syncIntervalMinutes },
|
||||
request,
|
||||
})
|
||||
|
||||
dispatchSync(connectorId, { requestId }).catch((error) => {
|
||||
logger.error(
|
||||
`[${requestId}] Failed to dispatch initial sync for connector ${connectorId}`,
|
||||
|
||||
@@ -3,7 +3,6 @@ import { knowledgeBase } from '@sim/db/schema'
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { eq } from 'drizzle-orm'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { restoreKnowledgeBase } from '@/lib/knowledge/service'
|
||||
@@ -24,7 +23,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
const [kb] = await db
|
||||
.select({
|
||||
id: knowledgeBase.id,
|
||||
name: knowledgeBase.name,
|
||||
workspaceId: knowledgeBase.workspaceId,
|
||||
userId: knowledgeBase.userId,
|
||||
})
|
||||
@@ -49,19 +47,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Restored knowledge base ${id}`)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: kb.workspaceId,
|
||||
actorId: auth.userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.KNOWLEDGE_BASE_RESTORED,
|
||||
resourceType: AuditResourceType.KNOWLEDGE_BASE,
|
||||
resourceId: id,
|
||||
resourceName: kb.name,
|
||||
description: `Restored knowledge base "${kb.name}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error restoring knowledge base ${id}`, error)
|
||||
|
||||
@@ -27,34 +27,6 @@ export async function GET(request: NextRequest) {
|
||||
try {
|
||||
const now = new Date()
|
||||
|
||||
const STALE_SYNC_TTL_MS = 120 * 60 * 1000
|
||||
const staleCutoff = new Date(now.getTime() - STALE_SYNC_TTL_MS)
|
||||
|
||||
const recoveredConnectors = await db
|
||||
.update(knowledgeConnector)
|
||||
.set({
|
||||
status: 'error',
|
||||
lastSyncError: 'Sync timed out (stale lock recovered)',
|
||||
nextSyncAt: new Date(now.getTime() + 10 * 60 * 1000),
|
||||
updatedAt: now,
|
||||
})
|
||||
.where(
|
||||
and(
|
||||
eq(knowledgeConnector.status, 'syncing'),
|
||||
lte(knowledgeConnector.updatedAt, staleCutoff),
|
||||
isNull(knowledgeConnector.archivedAt),
|
||||
isNull(knowledgeConnector.deletedAt)
|
||||
)
|
||||
)
|
||||
.returning({ id: knowledgeConnector.id })
|
||||
|
||||
if (recoveredConnectors.length > 0) {
|
||||
logger.warn(
|
||||
`[${requestId}] Recovered ${recoveredConnectors.length} stale syncing connectors`,
|
||||
{ ids: recoveredConnectors.map((c) => c.id) }
|
||||
)
|
||||
}
|
||||
|
||||
const dueConnectors = await db
|
||||
.select({
|
||||
id: knowledgeConnector.id,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getTableById, restoreTable } from '@/lib/table'
|
||||
@@ -35,19 +34,6 @@ export async function POST(
|
||||
|
||||
logger.info(`[${requestId}] Restored table ${tableId}`)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: table.workspaceId,
|
||||
actorId: auth.userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.TABLE_RESTORED,
|
||||
resourceType: AuditResourceType.TABLE,
|
||||
resourceId: tableId,
|
||||
resourceName: table.name,
|
||||
description: `Restored table "${table.name}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error restoring table ${tableId}`, error)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { checkSessionOrInternalAuth } from '@/lib/auth/hybrid'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { restoreWorkflow } from '@/lib/workflows/lifecycle'
|
||||
@@ -45,19 +44,6 @@ export async function POST(request: NextRequest, { params }: { params: Promise<{
|
||||
|
||||
logger.info(`[${requestId}] Restored workflow ${workflowId}`)
|
||||
|
||||
recordAudit({
|
||||
workspaceId: workflowData.workspaceId,
|
||||
actorId: auth.userId,
|
||||
actorName: auth.userName,
|
||||
actorEmail: auth.userEmail,
|
||||
action: AuditAction.WORKFLOW_RESTORED,
|
||||
resourceType: AuditResourceType.WORKFLOW,
|
||||
resourceId: workflowId,
|
||||
resourceName: workflowData.name,
|
||||
description: `Restored workflow "${workflowData.name}"`,
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error restoring workflow ${workflowId}`, error)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { createLogger } from '@sim/logger'
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { AuditAction, AuditResourceType, recordAudit } from '@/lib/audit/log'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { restoreWorkspaceFile } from '@/lib/uploads/contexts/workspace'
|
||||
@@ -30,19 +29,6 @@ export async function POST(
|
||||
|
||||
logger.info(`[${requestId}] Restored workspace file ${fileId}`)
|
||||
|
||||
recordAudit({
|
||||
workspaceId,
|
||||
actorId: session.user.id,
|
||||
actorName: session.user.name,
|
||||
actorEmail: session.user.email,
|
||||
action: AuditAction.FILE_RESTORED,
|
||||
resourceType: AuditResourceType.FILE,
|
||||
resourceId: fileId,
|
||||
resourceName: fileId,
|
||||
description: `Restored workspace file ${fileId}`,
|
||||
request,
|
||||
})
|
||||
|
||||
return NextResponse.json({ success: true })
|
||||
} catch (error) {
|
||||
logger.error(`[${requestId}] Error restoring workspace file ${fileId}`, error)
|
||||
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function ChangelogLoading() {
|
||||
return (
|
||||
<div className='min-h-screen'>
|
||||
<div className='relative grid md:grid-cols-2'>
|
||||
<div className='relative top-0 overflow-hidden border-[#2A2A2A] border-b px-6 py-16 sm:px-10 md:sticky md:h-dvh md:border-r md:border-b-0 md:px-12 md:py-24'>
|
||||
<div className='relative mx-auto h-full max-w-xl md:flex md:flex-col md:justify-center'>
|
||||
<Skeleton className='mt-6 h-[48px] w-[200px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='mt-4 h-[14px] w-[300px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='mt-[4px] h-[14px] w-[260px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='mt-6 h-[1px] w-full bg-[#2A2A2A]' />
|
||||
<div className='mt-6 flex flex-wrap items-center gap-3'>
|
||||
<Skeleton className='h-[32px] w-[130px] rounded-[5px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[32px] w-[120px] rounded-[5px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[32px] w-[80px] rounded-[5px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='px-6 py-16 sm:px-10 md:px-12 md:py-24'>
|
||||
<div className='max-w-2xl space-y-[32px]'>
|
||||
{Array.from({ length: 4 }).map((_, i) => (
|
||||
<div key={i} className='space-y-[12px]'>
|
||||
<Skeleton className='h-[20px] w-[160px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[14px] w-[100px] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<div className='space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-full rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[14px] w-[90%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
<Skeleton className='h-[14px] w-[75%] rounded-[4px] bg-[#2A2A2A]' />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function ChatLoading() {
|
||||
return (
|
||||
<div className='fixed inset-0 z-[100] flex flex-col bg-white text-foreground'>
|
||||
<div className='border-b px-4 py-3'>
|
||||
<div className='mx-auto flex max-w-3xl items-center justify-between'>
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[28px] w-[28px] rounded-[6px]' />
|
||||
<Skeleton className='h-[18px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
<Skeleton className='h-[28px] w-[80px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex min-h-0 flex-1 items-center justify-center px-4'>
|
||||
<div className='w-full max-w-[410px]'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='space-y-2 text-center'>
|
||||
<Skeleton className='mx-auto h-8 w-32' />
|
||||
<Skeleton className='mx-auto h-4 w-48' />
|
||||
</div>
|
||||
<div className='mt-8 w-full space-y-8'>
|
||||
<div className='space-y-2'>
|
||||
<Skeleton className='h-4 w-16' />
|
||||
<Skeleton className='h-10 w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='h-10 w-full rounded-[10px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='relative p-3 pb-4 md:p-4 md:pb-6'>
|
||||
<div className='relative mx-auto max-w-3xl md:max-w-[748px]'>
|
||||
<Skeleton className='h-[48px] w-full rounded-[12px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function CredentialAccountLoading() {
|
||||
return (
|
||||
<main className='relative flex min-h-screen flex-col text-foreground'>
|
||||
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
|
||||
<div className='w-full max-w-lg px-4'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
|
||||
<Skeleton className='mt-[16px] h-[24px] w-[200px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[8px] h-[14px] w-[280px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[4px] h-[14px] w-[240px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[24px] h-[44px] w-[200px] rounded-[10px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function FormLoading() {
|
||||
return (
|
||||
<main className='relative flex min-h-screen flex-col text-foreground'>
|
||||
<div className='relative z-30 flex flex-1 items-center justify-center px-4 pb-24'>
|
||||
<div className='w-full max-w-[410px]'>
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<div className='space-y-2 text-center'>
|
||||
<Skeleton className='mx-auto h-8 w-32' />
|
||||
<Skeleton className='mx-auto h-4 w-48' />
|
||||
</div>
|
||||
<div className='mt-8 w-full space-y-8'>
|
||||
<div className='space-y-2'>
|
||||
<Skeleton className='h-4 w-16' />
|
||||
<Skeleton className='h-10 w-full rounded-[10px]' />
|
||||
</div>
|
||||
<div className='space-y-2'>
|
||||
<Skeleton className='h-4 w-20' />
|
||||
<Skeleton className='h-10 w-full rounded-[10px]' />
|
||||
</div>
|
||||
<Skeleton className='h-10 w-full rounded-[10px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function InviteLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
|
||||
<Skeleton className='mt-[16px] h-[24px] w-[200px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[8px] h-[14px] w-[280px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[4px] h-[14px] w-[240px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[24px] h-[44px] w-[200px] rounded-[10px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function ResumeLoading() {
|
||||
return (
|
||||
<div className='min-h-screen bg-background'>
|
||||
<div className='border-b px-4 py-3'>
|
||||
<div className='mx-auto flex max-w-[1200px] items-center justify-between'>
|
||||
<Skeleton className='h-[24px] w-[80px] rounded-[4px]' />
|
||||
<Skeleton className='h-[28px] w-[100px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-auto max-w-[1200px] px-6 py-8'>
|
||||
<div className='grid grid-cols-[280px_1fr] gap-6'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Skeleton className='h-[20px] w-[120px] rounded-[4px]' />
|
||||
{Array.from({ length: 3 }).map((_, i) => (
|
||||
<Skeleton key={i} className='h-[48px] w-full rounded-[8px]' />
|
||||
))}
|
||||
</div>
|
||||
<div className='rounded-[8px] border p-6'>
|
||||
<Skeleton className='h-[24px] w-[200px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[12px] h-[16px] w-[320px] rounded-[4px]' />
|
||||
<div className='mt-[24px] space-y-[16px]'>
|
||||
<div className='space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[80px] rounded-[4px]' />
|
||||
<Skeleton className='h-[40px] w-full rounded-[8px]' />
|
||||
</div>
|
||||
<div className='space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[100px] rounded-[4px]' />
|
||||
<Skeleton className='h-[80px] w-full rounded-[8px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-[24px] flex gap-[12px]'>
|
||||
<Skeleton className='h-[40px] w-[120px] rounded-[8px]' />
|
||||
<Skeleton className='h-[40px] w-[120px] rounded-[8px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function TemplateDetailLoading() {
|
||||
return (
|
||||
<div className='flex min-h-screen flex-col'>
|
||||
<div className='border-b px-6 py-3'>
|
||||
<div className='mx-auto flex max-w-[1200px] items-center justify-between'>
|
||||
<Skeleton className='h-[24px] w-[80px] rounded-[4px]' />
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
|
||||
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-auto w-full max-w-[1200px] px-6 pt-[24px] pb-[24px]'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[72px] rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
|
||||
<div className='mt-[14px] flex items-center justify-between'>
|
||||
<Skeleton className='h-[27px] w-[250px] rounded-[4px]' />
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
|
||||
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className='mt-[4px] h-[16px] w-[360px] rounded-[4px]' />
|
||||
<div className='mt-[16px] flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[16px] w-[48px] rounded-[4px]' />
|
||||
<Skeleton className='h-[16px] w-[48px] rounded-[4px]' />
|
||||
<Skeleton className='h-[16px] w-[1px] rounded-[1px]' />
|
||||
<Skeleton className='h-[20px] w-[20px] rounded-full' />
|
||||
<Skeleton className='h-[16px] w-[80px] rounded-[4px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[450px] w-full rounded-[8px]' />
|
||||
<Skeleton className='mt-[32px] h-[20px] w-[180px] rounded-[4px]' />
|
||||
<div className='mt-[12px] space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-full rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[85%] rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[70%] rounded-[4px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[32px] h-[20px] w-[160px] rounded-[4px]' />
|
||||
<div className='mt-[12px] flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[48px] w-[48px] rounded-full' />
|
||||
<div className='space-y-[6px]'>
|
||||
<Skeleton className='h-[16px] w-[120px] rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[200px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,36 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_CARD_COUNT = 8
|
||||
|
||||
export default function TemplatesLoading() {
|
||||
return (
|
||||
<div className='min-h-screen bg-white'>
|
||||
<div className='border-b px-6 py-3'>
|
||||
<div className='mx-auto flex max-w-[1200px] items-center justify-between'>
|
||||
<Skeleton className='h-[24px] w-[80px] rounded-[4px]' />
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
|
||||
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='mx-auto max-w-[1200px] px-6 py-8'>
|
||||
<Skeleton className='h-[40px] w-[400px] rounded-[8px]' />
|
||||
<div className='mt-[16px] flex gap-[8px]'>
|
||||
<Skeleton className='h-[32px] w-[64px] rounded-[6px]' />
|
||||
</div>
|
||||
<div className='mt-[24px] grid grid-cols-1 gap-x-[20px] gap-y-[40px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
|
||||
{Array.from({ length: SKELETON_CARD_COUNT }).map((_, i) => (
|
||||
<div key={i} className='h-[268px] w-full rounded-[8px] bg-[var(--surface-3)] p-[8px]'>
|
||||
<Skeleton className='h-[180px] w-full rounded-[6px]' />
|
||||
<div className='mt-[10px] px-[4px]'>
|
||||
<Skeleton className='h-[14px] w-[120px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[6px] h-[12px] w-[180px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function UnsubscribeLoading() {
|
||||
return (
|
||||
<div className='flex flex-col items-center justify-center'>
|
||||
<Skeleton className='h-[48px] w-[48px] rounded-[12px]' />
|
||||
<Skeleton className='mt-[16px] h-[24px] w-[180px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[8px] h-[14px] w-[300px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[4px] h-[14px] w-[260px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[24px] h-[44px] w-[200px] rounded-[10px]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export default function FileViewLoading() {
|
||||
return (
|
||||
<div className='fixed inset-0 z-50 flex items-center justify-center bg-[var(--bg)]'>
|
||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,62 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_ROW_COUNT = 5
|
||||
const COLUMN_COUNT = 6
|
||||
|
||||
export default function FilesLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[32px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[28px] w-[72px] rounded-[6px]' />
|
||||
<Skeleton className='h-[28px] w-[64px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='ml-[10px] h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='border-[var(--border)] border-b'>
|
||||
<th className='w-[40px] px-[12px] py-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</th>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
|
||||
<th key={i} className='px-[12px] py-[8px] text-left'>
|
||||
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className='border-[var(--border)] border-b'>
|
||||
<td className='w-[40px] px-[12px] py-[10px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</td>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
|
||||
<td key={colIndex} className='px-[12px] py-[10px]'>
|
||||
<Skeleton
|
||||
className='h-[14px] rounded-[4px]'
|
||||
style={{ width: colIndex === 0 ? '128px' : '80px' }}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_LINE_COUNT = 4
|
||||
|
||||
export default function HomeLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-col bg-[var(--bg)]'>
|
||||
<div className='min-h-0 flex-1 overflow-hidden px-6 py-4'>
|
||||
<div className='mx-auto max-w-[42rem] space-y-[10px] pt-3'>
|
||||
{Array.from({ length: SKELETON_LINE_COUNT }).map((_, i) => (
|
||||
<Skeleton key={i} className='h-[16px]' style={{ width: `${120 + (i % 4) * 48}px` }} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div className='flex-shrink-0 px-[24px] pb-[16px]'>
|
||||
<div className='mx-auto max-w-[42rem]'>
|
||||
<Skeleton className='h-[48px] w-full rounded-[12px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_ROW_COUNT = 5
|
||||
const COLUMN_COUNT = 4
|
||||
|
||||
export default function DocumentLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='border-[var(--border)] border-b px-[16px] py-[8.5px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[96px] rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[100px] rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[80px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='ml-[10px] h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='border-[var(--border)] border-b'>
|
||||
<th className='w-[40px] px-[12px] py-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</th>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
|
||||
<th key={i} className='px-[12px] py-[8px] text-left'>
|
||||
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className='border-[var(--border)] border-b'>
|
||||
<td className='w-[40px] px-[12px] py-[10px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</td>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
|
||||
<td key={colIndex} className='px-[12px] py-[10px]'>
|
||||
<Skeleton
|
||||
className='h-[14px] rounded-[4px]'
|
||||
style={{ width: colIndex === 0 ? '200px' : '80px' }}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,69 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_ROW_COUNT = 5
|
||||
const COLUMN_COUNT = 7
|
||||
|
||||
export default function KnowledgeBaseLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='border-[var(--border)] border-b px-[16px] py-[8.5px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[96px] rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[28px] w-[112px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='ml-[10px] h-[14px] w-[140px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
|
||||
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='border-[var(--border)] border-b'>
|
||||
<th className='w-[40px] px-[12px] py-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</th>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
|
||||
<th key={i} className='px-[12px] py-[8px] text-left'>
|
||||
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className='border-[var(--border)] border-b'>
|
||||
<td className='w-[40px] px-[12px] py-[10px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</td>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
|
||||
<td key={colIndex} className='px-[12px] py-[10px]'>
|
||||
<Skeleton
|
||||
className='h-[14px] rounded-[4px]'
|
||||
style={{ width: colIndex === 0 ? '128px' : '80px' }}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_ROW_COUNT = 5
|
||||
const COLUMN_COUNT = 6
|
||||
|
||||
export default function KnowledgeLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[96px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[28px] w-[80px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='ml-[10px] h-[14px] w-[160px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='border-[var(--border)] border-b'>
|
||||
<th className='w-[40px] px-[12px] py-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</th>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
|
||||
<th key={i} className='px-[12px] py-[8px] text-left'>
|
||||
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className='border-[var(--border)] border-b'>
|
||||
<td className='w-[40px] px-[12px] py-[10px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</td>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
|
||||
<td key={colIndex} className='px-[12px] py-[10px]'>
|
||||
<Skeleton
|
||||
className='h-[14px] rounded-[4px]'
|
||||
style={{ width: colIndex === 0 ? '128px' : '80px' }}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,71 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_ROW_COUNT = 5
|
||||
const COLUMN_COUNT = 6
|
||||
|
||||
export default function LogsLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[32px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[28px] w-[64px] rounded-[6px]' />
|
||||
<Skeleton className='h-[28px] w-[96px] rounded-[6px]' />
|
||||
<Skeleton className='h-[28px] w-[28px] rounded-[6px]' />
|
||||
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
|
||||
<Skeleton className='h-[28px] w-[72px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='ml-[10px] h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
|
||||
<Skeleton className='h-[28px] w-[56px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='border-[var(--border)] border-b'>
|
||||
<th className='w-[40px] px-[12px] py-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</th>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
|
||||
<th key={i} className='px-[12px] py-[8px] text-left'>
|
||||
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className='border-[var(--border)] border-b'>
|
||||
<td className='w-[40px] px-[12px] py-[10px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</td>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
|
||||
<td key={colIndex} className='px-[12px] py-[10px]'>
|
||||
<Skeleton
|
||||
className='h-[14px] rounded-[4px]'
|
||||
style={{ width: colIndex === 0 ? '128px' : '80px' }}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_ROW_COUNT = 5
|
||||
const COLUMN_COUNT = 4
|
||||
|
||||
export default function ScheduledTasksLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[104px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[28px] w-[136px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='ml-[10px] h-[14px] w-[160px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='border-[var(--border)] border-b'>
|
||||
<th className='w-[40px] px-[12px] py-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</th>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
|
||||
<th key={i} className='px-[12px] py-[8px] text-left'>
|
||||
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className='border-[var(--border)] border-b'>
|
||||
<td className='w-[40px] px-[12px] py-[10px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</td>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
|
||||
<td key={colIndex} className='px-[12px] py-[10px]'>
|
||||
<Skeleton
|
||||
className='h-[14px] rounded-[4px]'
|
||||
style={{
|
||||
width: colIndex === 0 ? '128px' : colIndex === 1 ? '160px' : '80px',
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,15 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function SettingsLoading() {
|
||||
return (
|
||||
<div>
|
||||
<Skeleton className='mb-[28px] h-[28px] w-[140px] rounded-[4px]' />
|
||||
<div className='flex flex-col gap-[16px]'>
|
||||
<Skeleton className='h-[20px] w-[200px] rounded-[4px]' />
|
||||
<Skeleton className='h-[40px] w-full rounded-[8px]' />
|
||||
<Skeleton className='h-[40px] w-full rounded-[8px]' />
|
||||
<Skeleton className='h-[40px] w-full rounded-[8px]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_ROW_COUNT = 8
|
||||
const COLUMN_COUNT = 5
|
||||
|
||||
export default function TableDetailLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='border-[var(--border)] border-b px-[16px] py-[8.5px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[44px] rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[100px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[28px] w-[80px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='border-[var(--border)] border-b'>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
|
||||
<th key={i} className='px-[12px] py-[8px] text-left'>
|
||||
<Skeleton className='h-[12px] w-[72px] rounded-[4px]' />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className='border-[var(--border)] border-b'>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
|
||||
<td key={colIndex} className='px-[12px] py-[10px]'>
|
||||
<Skeleton
|
||||
className='h-[14px] rounded-[4px]'
|
||||
style={{ width: `${80 + (colIndex % 3) * 40}px` }}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_ROW_COUNT = 5
|
||||
const COLUMN_COUNT = 6
|
||||
|
||||
export default function TablesLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center justify-between'>
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[44px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='flex items-center gap-[6px]'>
|
||||
<Skeleton className='h-[28px] w-[80px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className='border-[var(--border)] border-b px-[24px] py-[10px]'>
|
||||
<div className='flex items-center'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
<Skeleton className='ml-[10px] h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='min-h-0 flex-1 overflow-auto'>
|
||||
<table className='w-full'>
|
||||
<thead>
|
||||
<tr className='border-[var(--border)] border-b'>
|
||||
<th className='w-[40px] px-[12px] py-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</th>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, i) => (
|
||||
<th key={i} className='px-[12px] py-[8px] text-left'>
|
||||
<Skeleton className='h-[12px] w-[56px] rounded-[4px]' />
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{Array.from({ length: SKELETON_ROW_COUNT }).map((_, rowIndex) => (
|
||||
<tr key={rowIndex} className='border-[var(--border)] border-b'>
|
||||
<td className='w-[40px] px-[12px] py-[10px]'>
|
||||
<Skeleton className='h-[14px] w-[14px] rounded-[2px]' />
|
||||
</td>
|
||||
{Array.from({ length: COLUMN_COUNT }).map((_, colIndex) => (
|
||||
<td key={colIndex} className='px-[12px] py-[10px]'>
|
||||
<Skeleton
|
||||
className='h-[14px] rounded-[4px]'
|
||||
style={{ width: colIndex === 0 ? '128px' : '80px' }}
|
||||
/>
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
export default function TemplateDetailLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col overflow-hidden'>
|
||||
<div className='flex flex-1 overflow-hidden'>
|
||||
<div className='flex flex-1 flex-col overflow-auto bg-white px-[24px] pt-[24px] pb-[24px] dark:bg-[var(--bg)]'>
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[14px] w-[72px] rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[8px] rounded-[2px]' />
|
||||
<Skeleton className='h-[14px] w-[120px] rounded-[4px]' />
|
||||
</div>
|
||||
<div className='mt-[14px] flex items-center justify-between'>
|
||||
<Skeleton className='h-[27px] w-[250px] rounded-[4px]' />
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
|
||||
<Skeleton className='h-[32px] w-[80px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
<Skeleton className='mt-[4px] h-[16px] w-[360px] rounded-[4px]' />
|
||||
<div className='mt-[16px] flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[16px] w-[48px] rounded-[4px]' />
|
||||
<Skeleton className='h-[16px] w-[48px] rounded-[4px]' />
|
||||
<Skeleton className='h-[16px] w-[1px] rounded-[1px]' />
|
||||
<Skeleton className='h-[20px] w-[20px] rounded-full' />
|
||||
<Skeleton className='h-[16px] w-[80px] rounded-[4px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[24px] h-[450px] w-full rounded-[8px]' />
|
||||
<Skeleton className='mt-[32px] h-[20px] w-[180px] rounded-[4px]' />
|
||||
<div className='mt-[12px] space-y-[8px]'>
|
||||
<Skeleton className='h-[14px] w-full rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[85%] rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[70%] rounded-[4px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[32px] h-[20px] w-[160px] rounded-[4px]' />
|
||||
<div className='mt-[12px] flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[48px] w-[48px] rounded-full' />
|
||||
<div className='space-y-[6px]'>
|
||||
<Skeleton className='h-[16px] w-[120px] rounded-[4px]' />
|
||||
<Skeleton className='h-[14px] w-[200px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,38 +0,0 @@
|
||||
import { Skeleton } from '@/components/emcn'
|
||||
|
||||
const SKELETON_CARD_COUNT = 8
|
||||
|
||||
export default function TemplatesLoading() {
|
||||
return (
|
||||
<div className='flex h-full flex-1 flex-col'>
|
||||
<div className='flex flex-1 overflow-hidden'>
|
||||
<div className='flex flex-1 flex-col overflow-auto bg-[var(--bg)] px-[24px] pt-[28px] pb-[24px]'>
|
||||
<div className='flex items-center gap-[12px]'>
|
||||
<Skeleton className='h-[26px] w-[26px] rounded-[6px]' />
|
||||
<Skeleton className='h-[22px] w-[80px] rounded-[4px]' />
|
||||
</div>
|
||||
<Skeleton className='mt-[6px] h-[14px] w-[280px] rounded-[4px]' />
|
||||
|
||||
<div className='mt-[14px] flex items-center justify-between'>
|
||||
<Skeleton className='h-[32px] w-[400px] rounded-[8px]' />
|
||||
<div className='flex items-center gap-[8px]'>
|
||||
<Skeleton className='h-[32px] w-[64px] rounded-[6px]' />
|
||||
<Skeleton className='h-[32px] w-[100px] rounded-[6px]' />
|
||||
</div>
|
||||
</div>
|
||||
<div className='mt-[24px] grid grid-cols-1 gap-x-[20px] gap-y-[40px] md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4'>
|
||||
{Array.from({ length: SKELETON_CARD_COUNT }).map((_, i) => (
|
||||
<div key={i} className='h-[268px] w-full rounded-[8px] bg-[var(--surface-3)] p-[8px]'>
|
||||
<Skeleton className='h-[180px] w-full rounded-[6px]' />
|
||||
<div className='mt-[10px] px-[4px]'>
|
||||
<Skeleton className='h-[14px] w-[120px] rounded-[4px]' />
|
||||
<Skeleton className='mt-[6px] h-[12px] w-[180px] rounded-[4px]' />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export default function WorkflowLoading() {
|
||||
return (
|
||||
<div className='flex h-full w-full flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='relative flex h-full w-full flex-1 items-center justify-center bg-[var(--bg)]'>
|
||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -201,6 +201,8 @@ const edgeTypes: EdgeTypes = {
|
||||
const defaultEdgeOptions = { type: 'custom' }
|
||||
|
||||
const reactFlowStyles = [
|
||||
'[&_.react-flow__edges]:!z-0',
|
||||
'[&_.react-flow__node]:z-[21]',
|
||||
'[&_.react-flow__handle]:!z-[30]',
|
||||
'[&_.react-flow__edge-labels]:!z-[1001]',
|
||||
'[&_.react-flow__pane]:select-none',
|
||||
@@ -2476,7 +2478,6 @@ const WorkflowContent = React.memo(
|
||||
|
||||
// Local state for nodes - allows smooth drag without store updates on every frame
|
||||
const [displayNodes, setDisplayNodes] = useState<Node[]>([])
|
||||
const [lastInteractedNodeId, setLastInteractedNodeId] = useState<string | null>(null)
|
||||
|
||||
const selectedNodeIds = useMemo(
|
||||
() => displayNodes.filter((node) => node.selected).map((node) => node.id),
|
||||
@@ -2488,14 +2489,6 @@ const WorkflowContent = React.memo(
|
||||
syncPanelWithSelection(selectedNodeIds)
|
||||
}, [selectedNodeIdsKey])
|
||||
|
||||
// Keep the most recently selected block on top even after deselection, so a
|
||||
// dragged block doesn't suddenly drop behind other overlapping blocks.
|
||||
useEffect(() => {
|
||||
if (selectedNodeIds.length > 0) {
|
||||
setLastInteractedNodeId(selectedNodeIds[selectedNodeIds.length - 1])
|
||||
}
|
||||
}, [selectedNodeIdsKey])
|
||||
|
||||
useEffect(() => {
|
||||
// Check for pending selection (from paste/duplicate), otherwise preserve existing selection
|
||||
if (pendingSelection && pendingSelection.length > 0) {
|
||||
@@ -3730,58 +3723,18 @@ const WorkflowContent = React.memo(
|
||||
[removeEdge, edges, blocks, addNotification, activeWorkflowId]
|
||||
)
|
||||
|
||||
// Elevate nodes using React Flow's native zIndex so selected/recent blocks
|
||||
// always sit above edges and other blocks.
|
||||
//
|
||||
// Z-index layers (regular blocks):
|
||||
// 21 — default
|
||||
// 22 — last interacted (dragged/selected, now deselected) so it stays on
|
||||
// top of siblings until another block is touched
|
||||
// 31 — currently selected (above connected edges at z-22 and handles at z-30)
|
||||
//
|
||||
// Subflow container nodes are skipped — they use depth-based zIndex for
|
||||
// correct parent/child layering and must not be bumped.
|
||||
// Child blocks inside containers already carry zIndex 1000 and are bumped by
|
||||
// +10 when selected so they stay above their sibling child blocks.
|
||||
const nodesForRender = useMemo(() => {
|
||||
return displayNodes.map((node) => {
|
||||
if (node.type === 'subflowNode') return node
|
||||
const base = node.zIndex ?? 21
|
||||
const target = node.selected
|
||||
? base + 10
|
||||
: node.id === lastInteractedNodeId
|
||||
? Math.max(base + 1, 22)
|
||||
: base
|
||||
if (target === (node.zIndex ?? 21)) return node
|
||||
return { ...node, zIndex: target }
|
||||
})
|
||||
}, [displayNodes, lastInteractedNodeId])
|
||||
|
||||
/** Transforms edges to include selection state and delete handlers. Memoized to prevent re-renders. */
|
||||
const edgesWithSelection = useMemo(() => {
|
||||
const nodeMap = new Map(displayNodes.map((n) => [n.id, n]))
|
||||
const elevatedNodeIdSet = new Set(
|
||||
lastInteractedNodeId ? [...selectedNodeIds, lastInteractedNodeId] : selectedNodeIds
|
||||
)
|
||||
|
||||
return edgesForDisplay.map((edge) => {
|
||||
const sourceNode = nodeMap.get(edge.source)
|
||||
const targetNode = nodeMap.get(edge.target)
|
||||
const parentLoopId = sourceNode?.parentId || targetNode?.parentId
|
||||
const edgeContextId = `${edge.id}${parentLoopId ? `-${parentLoopId}` : ''}`
|
||||
const connectedToElevated =
|
||||
elevatedNodeIdSet.has(edge.source) || elevatedNodeIdSet.has(edge.target)
|
||||
// Derive elevated z-index from connected nodes so edges inside subflows
|
||||
// (child nodes at z-1000) stay above their sibling child blocks.
|
||||
const elevatedZIndex = Math.max(
|
||||
22,
|
||||
(sourceNode?.zIndex ?? 21) + 1,
|
||||
(targetNode?.zIndex ?? 21) + 1
|
||||
)
|
||||
|
||||
return {
|
||||
...edge,
|
||||
zIndex: connectedToElevated ? elevatedZIndex : 0,
|
||||
data: {
|
||||
...edge.data,
|
||||
isSelected: selectedEdges.has(edgeContextId),
|
||||
@@ -3792,14 +3745,7 @@ const WorkflowContent = React.memo(
|
||||
},
|
||||
}
|
||||
})
|
||||
}, [
|
||||
edgesForDisplay,
|
||||
displayNodes,
|
||||
selectedNodeIds,
|
||||
selectedEdges,
|
||||
handleEdgeDelete,
|
||||
lastInteractedNodeId,
|
||||
])
|
||||
}, [edgesForDisplay, displayNodes, selectedEdges, handleEdgeDelete])
|
||||
|
||||
/** Handles Delete/Backspace to remove selected edges or blocks. */
|
||||
useEffect(() => {
|
||||
@@ -3939,7 +3885,7 @@ const WorkflowContent = React.memo(
|
||||
{showTrainingModal && <TrainingModal />}
|
||||
|
||||
<ReactFlow
|
||||
nodes={nodesForRender}
|
||||
nodes={displayNodes}
|
||||
edges={edgesWithSelection}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
@@ -4006,7 +3952,7 @@ const WorkflowContent = React.memo(
|
||||
onNodeDragStart={effectivePermissions.canEdit ? onNodeDragStart : undefined}
|
||||
snapToGrid={snapToGrid}
|
||||
snapGrid={snapGrid}
|
||||
elevateEdgesOnSelect={false}
|
||||
elevateEdgesOnSelect={true}
|
||||
onlyRenderVisibleElements={false}
|
||||
deleteKeyCode={null}
|
||||
elevateNodesOnSelect={false}
|
||||
|
||||
@@ -4,11 +4,13 @@ import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuSub,
|
||||
DropdownMenuSubContent,
|
||||
DropdownMenuSubTrigger,
|
||||
DropdownMenuTrigger,
|
||||
} from '@/components/emcn'
|
||||
import { Plus } from '@/components/emcn/icons'
|
||||
import { cn } from '@/lib/core/utils/cn'
|
||||
import type { useHoverMenu } from '@/app/workspace/[workspaceId]/w/components/sidebar/hooks'
|
||||
import type { FolderTreeNode } from '@/stores/folders/types'
|
||||
@@ -21,6 +23,8 @@ interface CollapsedSidebarMenuProps {
|
||||
ariaLabel?: string
|
||||
children: React.ReactNode
|
||||
className?: string
|
||||
createLabel?: string
|
||||
onCreateClick?: () => void
|
||||
}
|
||||
|
||||
export function CollapsedSidebarMenu({
|
||||
@@ -30,6 +34,8 @@ export function CollapsedSidebarMenu({
|
||||
ariaLabel,
|
||||
children,
|
||||
className,
|
||||
createLabel,
|
||||
onCreateClick,
|
||||
}: CollapsedSidebarMenuProps) {
|
||||
return (
|
||||
<div className={cn('flex flex-col px-[8px]', className)}>
|
||||
@@ -54,6 +60,15 @@ export function CollapsedSidebarMenu({
|
||||
</DropdownMenuTrigger>
|
||||
</div>
|
||||
<DropdownMenuContent side='right' align='start' sideOffset={8} {...hover.contentProps}>
|
||||
{createLabel && onCreateClick && (
|
||||
<>
|
||||
<DropdownMenuItem onSelect={onCreateClick}>
|
||||
<Plus className='h-[14px] w-[14px]' />
|
||||
<span>{createLabel}</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
</>
|
||||
)}
|
||||
{children}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
@@ -1109,6 +1109,52 @@ export const Sidebar = memo(function Sidebar() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick-create button (collapsed only) */}
|
||||
{isCollapsed && showCollapsedContent && (
|
||||
<div className='flex flex-shrink-0 flex-col px-[8px] pt-[8px]'>
|
||||
<DropdownMenu>
|
||||
<Tooltip.Root>
|
||||
<Tooltip.Trigger asChild>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<button
|
||||
type='button'
|
||||
aria-label='Create new'
|
||||
className='mx-[2px] flex h-[30px] items-center justify-center rounded-[8px] px-[8px] hover:bg-[var(--surface-active)]'
|
||||
>
|
||||
<Plus className='h-[16px] w-[16px] flex-shrink-0 text-[var(--text-icon)]' />
|
||||
</button>
|
||||
</DropdownMenuTrigger>
|
||||
</Tooltip.Trigger>
|
||||
<Tooltip.Content side='right'>
|
||||
<p>Create new</p>
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
<DropdownMenuContent side='right' align='start' sideOffset={8}>
|
||||
<DropdownMenuItem
|
||||
onSelect={() => navigateToPage(`/workspace/${workspaceId}/home`)}
|
||||
>
|
||||
<Blimp className='h-[14px] w-[14px]' />
|
||||
<span>New task</span>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onSelect={handleCreateWorkflow}
|
||||
disabled={!canEdit || isCreatingWorkflow}
|
||||
>
|
||||
<div
|
||||
className='h-[14px] w-[14px] flex-shrink-0 rounded-[3px] border-[2px]'
|
||||
style={{
|
||||
backgroundColor: 'var(--text-icon)',
|
||||
borderColor: 'color-mix(in srgb, var(--text-icon) 60%, transparent)',
|
||||
backgroundClip: 'padding-box',
|
||||
}}
|
||||
/>
|
||||
<span>{isCreatingWorkflow ? 'Creating...' : 'New workflow'}</span>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Scrollable Tasks + Workflows */}
|
||||
<div
|
||||
ref={isCollapsed ? undefined : scrollContainerRef}
|
||||
@@ -1149,6 +1195,8 @@ export const Sidebar = memo(function Sidebar() {
|
||||
onClick={() => navigateToPage(`/workspace/${workspaceId}/home`)}
|
||||
ariaLabel='Tasks'
|
||||
className='mt-[6px]'
|
||||
createLabel='New task'
|
||||
onCreateClick={() => navigateToPage(`/workspace/${workspaceId}/home`)}
|
||||
>
|
||||
{tasksLoading ? (
|
||||
<DropdownMenuItem disabled>
|
||||
@@ -1318,6 +1366,8 @@ export const Sidebar = memo(function Sidebar() {
|
||||
onClick={handleCreateWorkflow}
|
||||
ariaLabel='Workflows'
|
||||
className='mt-[6px]'
|
||||
createLabel='New workflow'
|
||||
onCreateClick={canEdit ? handleCreateWorkflow : undefined}
|
||||
>
|
||||
{workflowsLoading && regularWorkflows.length === 0 ? (
|
||||
<DropdownMenuItem disabled>
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Loader2 } from 'lucide-react'
|
||||
|
||||
export default function WorkflowsLoading() {
|
||||
return (
|
||||
<div className='flex h-full w-full flex-col overflow-hidden bg-[var(--bg)]'>
|
||||
<div className='relative flex h-full w-full flex-1 items-center justify-center bg-[var(--bg)]'>
|
||||
<Loader2 className='h-[20px] w-[20px] animate-spin text-[var(--text-tertiary)]' />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
import { A2AIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
|
||||
@@ -64,8 +63,6 @@ export const A2ABlock: BlockConfig<A2AResponse> = {
|
||||
'Compatible with any A2A-compliant agent including LangGraph, Google ADK, and other Sim workflows.',
|
||||
docsLink: 'https://docs.sim.ai/blocks/a2a',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.DeveloperTools,
|
||||
tags: ['agentic', 'automation'],
|
||||
bgColor: '#4151B5',
|
||||
icon: A2AIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -2,7 +2,7 @@ import { createLogger } from '@sim/logger'
|
||||
import { AgentIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { getApiKeyCondition, getModelOptions, RESPONSE_FORMAT_WAND_CONFIG } from '@/blocks/utils'
|
||||
import {
|
||||
getBaseModelProviders,
|
||||
@@ -69,8 +69,6 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
|
||||
`,
|
||||
docsLink: 'https://docs.sim.ai/blocks/agent',
|
||||
category: 'blocks',
|
||||
integrationType: IntegrationType.AI,
|
||||
tags: ['llm', 'agentic', 'automation'],
|
||||
bgColor: 'var(--brand-primary-hex)',
|
||||
icon: AgentIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AhrefsIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { AhrefsResponse } from '@/tools/ahrefs/types'
|
||||
|
||||
export const AhrefsBlock: BlockConfig<AhrefsResponse> = {
|
||||
@@ -12,8 +12,6 @@ export const AhrefsBlock: BlockConfig<AhrefsResponse> = {
|
||||
'Integrate Ahrefs SEO tools into your workflow. Analyze domain ratings, backlinks, organic keywords, top pages, and more. Requires an Ahrefs Enterprise plan with API access.',
|
||||
docsLink: 'https://docs.ahrefs.com/docs/api/reference/introduction',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Analytics,
|
||||
tags: ['seo', 'marketing', 'data-analytics'],
|
||||
bgColor: '#E0E0E0',
|
||||
icon: AhrefsIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AirtableIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { AirtableResponse } from '@/tools/airtable/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
@@ -14,8 +14,6 @@ export const AirtableBlock: BlockConfig<AirtableResponse> = {
|
||||
'Integrates Airtable into the workflow. Can list bases, list tables (with schema), and create, get, list, or update records. Can also be used in trigger mode to trigger a workflow when an update is made to an Airtable table.',
|
||||
docsLink: 'https://docs.sim.ai/tools/airtable',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Databases,
|
||||
tags: ['spreadsheet', 'automation'],
|
||||
bgColor: '#E0E0E0',
|
||||
icon: AirtableIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AirweaveIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { AirweaveSearchResponse } from '@/tools/airweave/types'
|
||||
|
||||
export const AirweaveBlock: BlockConfig<AirweaveSearchResponse> = {
|
||||
@@ -12,8 +12,6 @@ export const AirweaveBlock: BlockConfig<AirweaveSearchResponse> = {
|
||||
'Search across your synced data sources using Airweave. Supports semantic search with hybrid, neural, or keyword retrieval strategies. Optionally generate AI-powered answers from search results.',
|
||||
docsLink: 'https://docs.airweave.ai',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Search,
|
||||
tags: ['vector-search', 'knowledge-base'],
|
||||
bgColor: '#6366F1',
|
||||
icon: AirweaveIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AlgoliaIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
export const AlgoliaBlock: BlockConfig = {
|
||||
type: 'algolia',
|
||||
@@ -10,8 +10,6 @@ export const AlgoliaBlock: BlockConfig = {
|
||||
'Integrate Algolia into your workflow. Search indices, manage records (add, update, delete, browse), configure index settings, and perform batch operations.',
|
||||
docsLink: 'https://docs.sim.ai/tools/algolia',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Search,
|
||||
tags: ['vector-search', 'knowledge-base'],
|
||||
bgColor: '#003DFF',
|
||||
icon: AlgoliaIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AmplitudeIcon } from '@/components/icons'
|
||||
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
|
||||
export const AmplitudeBlock: BlockConfig = {
|
||||
type: 'amplitude',
|
||||
@@ -9,8 +9,6 @@ export const AmplitudeBlock: BlockConfig = {
|
||||
'Integrate Amplitude into your workflow to track events, identify users and groups, search for users, query analytics, and retrieve revenue data.',
|
||||
docsLink: 'https://docs.sim.ai/tools/amplitude',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Analytics,
|
||||
tags: ['data-analytics', 'marketing'],
|
||||
bgColor: '#1B1F3B',
|
||||
icon: AmplitudeIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApiIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import type { RequestResponse } from '@/tools/http/types'
|
||||
|
||||
export const ApiBlock: BlockConfig<RequestResponse> = {
|
||||
@@ -14,8 +13,6 @@ export const ApiBlock: BlockConfig<RequestResponse> = {
|
||||
- Curl the endpoint yourself before filling out the API block to make sure it's working IF you have the necessary authentication headers. Clarify with the user if you need any additional headers.
|
||||
`,
|
||||
category: 'blocks',
|
||||
integrationType: IntegrationType.DeveloperTools,
|
||||
tags: ['automation', 'webhooks'],
|
||||
bgColor: '#2F55FF',
|
||||
icon: ApiIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApifyIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import type { RunActorResult } from '@/tools/apify/types'
|
||||
|
||||
export const ApifyBlock: BlockConfig<RunActorResult> = {
|
||||
@@ -11,8 +10,6 @@ export const ApifyBlock: BlockConfig<RunActorResult> = {
|
||||
'Integrate Apify into your workflow. Run any Apify actor with custom input and retrieve results. Supports both synchronous and asynchronous execution with automatic dataset fetching.',
|
||||
docsLink: 'https://docs.sim.ai/tools/apify',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Automation,
|
||||
tags: ['web-scraping', 'automation', 'data-analytics'],
|
||||
bgColor: '#E0E0E0',
|
||||
icon: ApifyIcon,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ApolloIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { ApolloResponse } from '@/tools/apollo/types'
|
||||
|
||||
export const ApolloBlock: BlockConfig<ApolloResponse> = {
|
||||
@@ -12,8 +12,6 @@ export const ApolloBlock: BlockConfig<ApolloResponse> = {
|
||||
'Integrates Apollo.io into the workflow. Search for people and companies, enrich contact data, manage your CRM contacts and accounts, add contacts to sequences, and create tasks.',
|
||||
docsLink: 'https://docs.sim.ai/tools/apollo',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.SalesIntelligence,
|
||||
tags: ['enrichment', 'sales-engagement'],
|
||||
bgColor: '#EBF212',
|
||||
icon: ApolloIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ArxivIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import type { ArxivResponse } from '@/tools/arxiv/types'
|
||||
|
||||
export const ArxivBlock: BlockConfig<ArxivResponse> = {
|
||||
@@ -11,8 +10,6 @@ export const ArxivBlock: BlockConfig<ArxivResponse> = {
|
||||
'Integrates ArXiv into the workflow. Can search for papers, get paper details, and get author papers. Does not require OAuth or an API key.',
|
||||
docsLink: 'https://docs.sim.ai/tools/arxiv',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Search,
|
||||
tags: ['document-processing', 'knowledge-base'],
|
||||
bgColor: '#E0E0E0',
|
||||
icon: ArxivIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { AsanaIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { AsanaResponse } from '@/tools/asana/types'
|
||||
|
||||
export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
@@ -12,8 +12,6 @@ export const AsanaBlock: BlockConfig<AsanaResponse> = {
|
||||
longDescription: 'Integrate Asana into the workflow. Can read, write, and update tasks.',
|
||||
docsLink: 'https://docs.sim.ai/tools/asana',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Productivity,
|
||||
tags: ['project-management', 'ticketing', 'automation'],
|
||||
bgColor: '#E0E0E0',
|
||||
icon: AsanaIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { AshbyIcon } from '@/components/icons'
|
||||
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const AshbyBlock: BlockConfig = {
|
||||
@@ -10,8 +10,6 @@ export const AshbyBlock: BlockConfig = {
|
||||
'Integrate Ashby into the workflow. Manage candidates (list, get, create, update, search, tag), applications (list, get, create, change stage), jobs (list, get), job postings (list, get), offers (list, get), notes (list, create), interviews (list), and reference data (sources, tags, archive reasons, custom fields, departments, locations, openings, users).',
|
||||
docsLink: 'https://docs.sim.ai/tools/ashby',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.HR,
|
||||
tags: ['hiring'],
|
||||
bgColor: '#5D4ED6',
|
||||
icon: AshbyIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { AttioIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { AttioResponse } from '@/tools/attio/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
@@ -12,8 +12,6 @@ export const AttioBlock: BlockConfig<AttioResponse> = {
|
||||
'Connect to Attio to manage CRM records (people, companies, custom objects), notes, tasks, lists, list entries, comments, workspace members, and webhooks.',
|
||||
docsLink: 'https://docs.sim.ai/tools/attio',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.CRM,
|
||||
tags: ['sales-engagement', 'enrichment'],
|
||||
bgColor: '#1D1E20',
|
||||
icon: AttioIcon,
|
||||
authMode: AuthMode.OAuth,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { BoxCompanyIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
|
||||
export const BoxBlock: BlockConfig = {
|
||||
@@ -12,8 +12,6 @@ export const BoxBlock: BlockConfig = {
|
||||
'Integrate Box into your workflow to manage files, folders, and e-signatures. Upload and download files, search content, create folders, send documents for e-signature, track signing status, and more.',
|
||||
docsLink: 'https://docs.sim.ai/tools/box',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.FileStorage,
|
||||
tags: ['cloud', 'content-management', 'e-signatures'],
|
||||
bgColor: '#FFFFFF',
|
||||
icon: BoxCompanyIcon,
|
||||
authMode: AuthMode.OAuth,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { BrandfetchIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { BrandfetchGetBrandResponse, BrandfetchSearchResponse } from '@/tools/brandfetch/types'
|
||||
|
||||
export const BrandfetchBlock: BlockConfig<BrandfetchGetBrandResponse | BrandfetchSearchResponse> = {
|
||||
@@ -11,8 +11,6 @@ export const BrandfetchBlock: BlockConfig<BrandfetchGetBrandResponse | Brandfetc
|
||||
'Integrate Brandfetch into your workflow. Retrieve brand logos, colors, fonts, and company data by domain, ticker, or name search.',
|
||||
docsLink: 'https://docs.sim.ai/tools/brandfetch',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.SalesIntelligence,
|
||||
tags: ['enrichment', 'marketing'],
|
||||
bgColor: '#000000',
|
||||
icon: BrandfetchIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { BrowserUseIcon } from '@/components/icons'
|
||||
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { BrowserUseResponse } from '@/tools/browser_use/types'
|
||||
|
||||
export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
|
||||
@@ -11,8 +11,6 @@ export const BrowserUseBlock: BlockConfig<BrowserUseResponse> = {
|
||||
'Integrate Browser Use into the workflow. Can navigate the web and perform actions as if a real user was interacting with the browser.',
|
||||
docsLink: 'https://docs.sim.ai/tools/browser_use',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Automation,
|
||||
tags: ['web-scraping', 'automation', 'agentic'],
|
||||
bgColor: '#181C1E',
|
||||
icon: BrowserUseIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CalComIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
@@ -14,8 +14,6 @@ export const CalComBlock: BlockConfig<ToolResponse> = {
|
||||
'Integrate Cal.com into your workflow. Create and manage bookings, event types, schedules, and check availability slots. Supports creating, listing, rescheduling, and canceling bookings, as well as managing event types and schedules. Can also trigger workflows based on Cal.com webhook events (booking created, cancelled, rescheduled). Connect your Cal.com account via OAuth.',
|
||||
docsLink: 'https://docs.sim.ai/tools/calcom',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Productivity,
|
||||
tags: ['scheduling', 'calendar', 'meeting'],
|
||||
bgColor: '#FFFFFE',
|
||||
icon: CalComIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CalendlyIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { ToolResponse } from '@/tools/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
@@ -14,8 +14,6 @@ export const CalendlyBlock: BlockConfig<ToolResponse> = {
|
||||
'Integrate Calendly into your workflow. Manage event types, scheduled events, invitees, and webhooks. Can also trigger workflows based on Calendly webhook events (invitee scheduled, invitee canceled, routing form submitted). Requires Personal Access Token.',
|
||||
docsLink: 'https://docs.sim.ai/tools/calendly',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Productivity,
|
||||
tags: ['scheduling', 'calendar', 'meeting'],
|
||||
bgColor: '#FFFFFF',
|
||||
icon: CalendlyIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { CirclebackIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
|
||||
export const CirclebackBlock: BlockConfig = {
|
||||
@@ -10,8 +9,6 @@ export const CirclebackBlock: BlockConfig = {
|
||||
longDescription:
|
||||
'Receive meeting notes, action items, transcripts, and recordings when meetings are processed. Circleback uses webhooks to push data to your workflows.',
|
||||
category: 'triggers',
|
||||
integrationType: IntegrationType.AI,
|
||||
tags: ['meeting', 'note-taking', 'automation'],
|
||||
bgColor: 'linear-gradient(180deg, #E0F7FA 0%, #FFFFFF 100%)',
|
||||
docsLink: 'https://docs.sim.ai/tools/circleback',
|
||||
icon: CirclebackIcon,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ClayIcon } from '@/components/icons'
|
||||
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { ClayPopulateResponse } from '@/tools/clay/types'
|
||||
|
||||
export const ClayBlock: BlockConfig<ClayPopulateResponse> = {
|
||||
@@ -10,8 +10,6 @@ export const ClayBlock: BlockConfig<ClayPopulateResponse> = {
|
||||
longDescription: 'Integrate Clay into the workflow. Can populate a table with data.',
|
||||
docsLink: 'https://docs.sim.ai/tools/clay',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.SalesIntelligence,
|
||||
tags: ['enrichment', 'sales-engagement', 'data-analytics'],
|
||||
bgColor: '#E0E0E0',
|
||||
icon: ClayIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ClerkIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import type { ClerkResponse } from '@/tools/clerk/types'
|
||||
|
||||
export const ClerkBlock: BlockConfig<ClerkResponse> = {
|
||||
@@ -11,8 +10,6 @@ export const ClerkBlock: BlockConfig<ClerkResponse> = {
|
||||
'Integrate Clerk authentication and user management into your workflow. Create, update, delete, and list users. Manage organizations and their memberships. Monitor and control user sessions.',
|
||||
docsLink: 'https://docs.sim.ai/tools/clerk',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Security,
|
||||
tags: ['identity', 'automation'],
|
||||
bgColor: '#131316',
|
||||
icon: ClerkIcon,
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { CloudflareIcon } from '@/components/icons'
|
||||
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { CloudflareResponse } from '@/tools/cloudflare/types'
|
||||
|
||||
export const CloudflareBlock: BlockConfig<CloudflareResponse> = {
|
||||
@@ -11,8 +11,6 @@ export const CloudflareBlock: BlockConfig<CloudflareResponse> = {
|
||||
'Integrate Cloudflare into the workflow. Manage zones (domains), DNS records, SSL/TLS certificates, zone settings, DNS analytics, and cache purging via the Cloudflare API.',
|
||||
docsLink: 'https://docs.sim.ai/tools/cloudflare',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.DeveloperTools,
|
||||
tags: ['cloud', 'monitoring'],
|
||||
bgColor: '#F5F6FA',
|
||||
icon: CloudflareIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { ConfluenceIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
import type { ConfluenceResponse } from '@/tools/confluence/types'
|
||||
import { getTrigger } from '@/triggers'
|
||||
@@ -16,8 +16,6 @@ export const ConfluenceBlock: BlockConfig<ConfluenceResponse> = {
|
||||
'Integrate Confluence into the workflow. Can read, create, update, delete pages, manage comments, attachments, labels, and search content.',
|
||||
docsLink: 'https://docs.sim.ai/tools/confluence',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Documents,
|
||||
tags: ['knowledge-base', 'content-management', 'note-taking'],
|
||||
bgColor: '#E0E0E0',
|
||||
icon: ConfluenceIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { CursorIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { createVersionedToolSelector } from '@/blocks/utils'
|
||||
import type { CursorResponse } from '@/tools/cursor/types'
|
||||
|
||||
@@ -12,8 +12,6 @@ export const CursorBlock: BlockConfig<CursorResponse> = {
|
||||
'Interact with Cursor Cloud Agents API to launch AI agents that can work on your GitHub repositories. Supports launching agents, adding follow-up instructions, checking status, viewing conversations, and managing agent lifecycle.',
|
||||
docsLink: 'https://cursor.com/docs/cloud-agent/api/endpoints',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.DeveloperTools,
|
||||
tags: ['agentic', 'automation'],
|
||||
bgColor: '#1E1E1E',
|
||||
icon: CursorIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DatabricksIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { DatabricksResponse } from '@/tools/databricks/types'
|
||||
|
||||
export const DatabricksBlock: BlockConfig<DatabricksResponse> = {
|
||||
@@ -12,8 +12,6 @@ export const DatabricksBlock: BlockConfig<DatabricksResponse> = {
|
||||
'Connect to Databricks to execute SQL queries against SQL warehouses, trigger and monitor job runs, manage clusters, and retrieve run outputs. Requires a Personal Access Token and workspace host URL.',
|
||||
docsLink: 'https://docs.sim.ai/tools/databricks',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Databases,
|
||||
tags: ['data-warehouse', 'data-analytics', 'cloud'],
|
||||
bgColor: '#F9F7F4',
|
||||
icon: DatabricksIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DatadogIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { DatadogResponse } from '@/tools/datadog/types'
|
||||
|
||||
export const DatadogBlock: BlockConfig<DatadogResponse> = {
|
||||
@@ -12,8 +12,6 @@ export const DatadogBlock: BlockConfig<DatadogResponse> = {
|
||||
'Integrate Datadog monitoring into workflows. Submit metrics, manage monitors, query logs, create events, handle downtimes, and more.',
|
||||
docsLink: 'https://docs.sim.ai/tools/datadog',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Analytics,
|
||||
tags: ['monitoring', 'incident-management', 'error-tracking'],
|
||||
bgColor: '#632CA6',
|
||||
icon: DatadogIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DevinIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
export const DevinBlock: BlockConfig = {
|
||||
type: 'devin',
|
||||
@@ -17,8 +17,6 @@ export const DevinBlock: BlockConfig = {
|
||||
`,
|
||||
docsLink: 'https://docs.sim.ai/tools/devin',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.DeveloperTools,
|
||||
tags: ['agentic', 'automation'],
|
||||
bgColor: '#12141A',
|
||||
icon: DevinIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DiscordIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
import type { DiscordResponse } from '@/tools/discord/types'
|
||||
|
||||
@@ -12,8 +12,6 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
|
||||
longDescription:
|
||||
'Comprehensive Discord integration: messages, threads, channels, roles, members, invites, and webhooks.',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Communication,
|
||||
tags: ['messaging', 'webhooks', 'automation'],
|
||||
bgColor: '#5865F2',
|
||||
icon: DiscordIcon,
|
||||
docsLink: 'https://docs.sim.ai/tools/discord',
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DocuSignIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
import type { DocuSignResponse } from '@/tools/docusign/types'
|
||||
|
||||
@@ -13,8 +13,6 @@ export const DocuSignBlock: BlockConfig<DocuSignResponse> = {
|
||||
'Create and send envelopes for e-signature, use templates, check signing status, download signed documents, and manage recipients with DocuSign.',
|
||||
docsLink: 'https://docs.sim.ai/tools/docusign',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Documents,
|
||||
tags: ['e-signatures', 'document-processing'],
|
||||
bgColor: '#FFFFFF',
|
||||
icon: DocuSignIcon,
|
||||
authMode: AuthMode.OAuth,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { DropboxIcon } from '@/components/icons'
|
||||
import { getScopesForService } from '@/lib/oauth/utils'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import { normalizeFileInput } from '@/blocks/utils'
|
||||
import type { DropboxResponse } from '@/tools/dropbox/types'
|
||||
|
||||
@@ -14,8 +14,6 @@ export const DropboxBlock: BlockConfig<DropboxResponse> = {
|
||||
'Integrate Dropbox into your workflow for file management, sharing, and collaboration. Upload files, download content, create folders, manage shared links, and more.',
|
||||
docsLink: 'https://docs.sim.ai/tools/dropbox',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.FileStorage,
|
||||
tags: ['cloud', 'document-processing'],
|
||||
icon: DropboxIcon,
|
||||
bgColor: '#0061FF',
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DsPyIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
|
||||
export const DSPyBlock: BlockConfig = {
|
||||
type: 'dspy',
|
||||
@@ -9,8 +8,6 @@ export const DSPyBlock: BlockConfig = {
|
||||
longDescription:
|
||||
'Integrate with your self-hosted DSPy programs for LLM-powered predictions. Supports Predict, Chain of Thought, and ReAct agents. DSPy is the framework for programming—not prompting—language models.',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.AI,
|
||||
tags: ['llm', 'agentic', 'automation'],
|
||||
bgColor: '#E0E0E0',
|
||||
icon: DsPyIcon,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { DubIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { DubResponse } from '@/tools/dub/types'
|
||||
|
||||
export const DubBlock: BlockConfig<DubResponse> = {
|
||||
@@ -12,8 +12,6 @@ export const DubBlock: BlockConfig<DubResponse> = {
|
||||
'Create, manage, and track short links with Dub. Supports custom domains, UTM parameters, link analytics, and more.',
|
||||
docsLink: 'https://docs.sim.ai/tools/dub',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.DeveloperTools,
|
||||
tags: ['link-management', 'marketing', 'data-analytics'],
|
||||
bgColor: '#181C1E',
|
||||
icon: DubIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DuckDuckGoIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import type { DuckDuckGoResponse } from '@/tools/duckduckgo/types'
|
||||
|
||||
export const DuckDuckGoBlock: BlockConfig<DuckDuckGoResponse> = {
|
||||
@@ -11,8 +10,6 @@ export const DuckDuckGoBlock: BlockConfig<DuckDuckGoResponse> = {
|
||||
'Search the web using DuckDuckGo Instant Answers API. Returns instant answers, abstracts, related topics, and more. Free to use without an API key.',
|
||||
docsLink: 'https://docs.sim.ai/tools/duckduckgo',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Search,
|
||||
tags: ['web-scraping', 'seo'],
|
||||
bgColor: '#FFFFFF',
|
||||
icon: DuckDuckGoIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { DynamoDBIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { IntegrationType } from '@/blocks/types'
|
||||
import type { DynamoDBIntrospectResponse, DynamoDBResponse } from '@/tools/dynamodb/types'
|
||||
|
||||
export const DynamoDBBlock: BlockConfig<DynamoDBResponse | DynamoDBIntrospectResponse> = {
|
||||
@@ -11,8 +10,6 @@ export const DynamoDBBlock: BlockConfig<DynamoDBResponse | DynamoDBIntrospectRes
|
||||
'Integrate Amazon DynamoDB into workflows. Supports Get, Put, Query, Scan, Update, Delete, and Introspect operations on DynamoDB tables.',
|
||||
docsLink: 'https://docs.sim.ai/tools/dynamodb',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Databases,
|
||||
tags: ['cloud', 'data-warehouse'],
|
||||
bgColor: 'linear-gradient(45deg, #2E27AD 0%, #527FFF 100%)',
|
||||
icon: DynamoDBIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { ElasticsearchIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
import type { ElasticsearchResponse } from '@/tools/elasticsearch/types'
|
||||
|
||||
export const ElasticsearchBlock: BlockConfig<ElasticsearchResponse> = {
|
||||
@@ -12,8 +12,6 @@ export const ElasticsearchBlock: BlockConfig<ElasticsearchResponse> = {
|
||||
'Integrate Elasticsearch into workflows for powerful search, indexing, and data management. Supports document CRUD operations, advanced search queries, bulk operations, index management, and cluster monitoring. Works with both self-hosted and Elastic Cloud deployments.',
|
||||
docsLink: 'https://docs.sim.ai/tools/elasticsearch',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Databases,
|
||||
tags: ['vector-search', 'data-analytics'],
|
||||
bgColor: '#E0E0E0',
|
||||
icon: ElasticsearchIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { ElevenLabsIcon } from '@/components/icons'
|
||||
import { AuthMode, type BlockConfig, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode, type BlockConfig } from '@/blocks/types'
|
||||
import type { ElevenLabsBlockResponse } from '@/tools/elevenlabs/types'
|
||||
|
||||
export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
|
||||
@@ -10,8 +10,6 @@ export const ElevenLabsBlock: BlockConfig<ElevenLabsBlockResponse> = {
|
||||
longDescription: 'Integrate ElevenLabs into the workflow. Can convert text to speech.',
|
||||
docsLink: 'https://docs.sim.ai/tools/elevenlabs',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Media,
|
||||
tags: ['text-to-speech'],
|
||||
bgColor: '#181C1E',
|
||||
icon: ElevenLabsIcon,
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EnrichSoIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
export const EnrichBlock: BlockConfig = {
|
||||
type: 'enrich',
|
||||
@@ -11,8 +11,6 @@ export const EnrichBlock: BlockConfig = {
|
||||
'Access real-time B2B data intelligence with Enrich.so. Enrich profiles from email addresses, find work emails from LinkedIn, verify email deliverability, search for people and companies, and analyze LinkedIn post engagement.',
|
||||
docsLink: 'https://docs.enrich.so/',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.SalesIntelligence,
|
||||
tags: ['enrichment', 'data-analytics'],
|
||||
bgColor: '#E5E5E6',
|
||||
icon: EnrichSoIcon,
|
||||
subBlocks: [
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { EvernoteIcon } from '@/components/icons'
|
||||
import type { BlockConfig } from '@/blocks/types'
|
||||
import { AuthMode, IntegrationType } from '@/blocks/types'
|
||||
import { AuthMode } from '@/blocks/types'
|
||||
|
||||
export const EvernoteBlock: BlockConfig = {
|
||||
type: 'evernote',
|
||||
@@ -10,8 +10,6 @@ export const EvernoteBlock: BlockConfig = {
|
||||
'Integrate with Evernote to manage notes, notebooks, and tags. Create, read, update, copy, search, and delete notes. Create and list notebooks and tags.',
|
||||
docsLink: 'https://docs.sim.ai/tools/evernote',
|
||||
category: 'tools',
|
||||
integrationType: IntegrationType.Documents,
|
||||
tags: ['note-taking', 'knowledge-base'],
|
||||
bgColor: '#E0E0E0',
|
||||
icon: EvernoteIcon,
|
||||
authMode: AuthMode.ApiKey,
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user