Skip to content

Commit 685ae54

Browse files
committed
feat(tables): render URL cells with favicon and clickable link
1 parent e7b5a01 commit 685ae54

1 file changed

Lines changed: 104 additions & 0 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table-grid/cells/cell-render.tsx

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ export type CellRenderKind =
3838
| { kind: 'boolean'; checked: boolean }
3939
| { kind: 'json'; text: string }
4040
| { kind: 'date'; text: string }
41+
| { kind: 'url'; text: string; href: string; domain: string }
4142
| { kind: 'text'; text: string }
4243
// Universal fallback
4344
| { kind: 'empty' }
@@ -113,6 +114,12 @@ export function resolveCellRender({
113114
if (isNull) return { kind: 'empty' }
114115
if (column.type === 'json') return { kind: 'json', text: JSON.stringify(value) }
115116
if (column.type === 'date') return { kind: 'date', text: String(value) }
117+
if (column.type === 'string') {
118+
const text = stringifyValue(value)
119+
const urlInfo = extractUrlInfo(text)
120+
if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain }
121+
return { kind: 'text', text }
122+
}
116123
return { kind: 'text', text: stringifyValue(value) }
117124
}
118125

@@ -122,6 +129,78 @@ function stringifyValue(value: unknown): string {
122129
return JSON.stringify(value)
123130
}
124131

132+
/** Matches bare hostnames: `microsoft.com`, `www.linkedin.com`, `sub.domain.co.uk` */
133+
const BARE_DOMAIN_RE = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/
134+
135+
/**
136+
* File extensions that are also valid TLDs but should never be treated as URLs.
137+
* Without this, strings like `config.json` or `readme.md` would resolve as domains.
138+
*/
139+
const FILE_EXTENSION_TLDS = new Set([
140+
'txt',
141+
'json',
142+
'yaml',
143+
'yml',
144+
'xml',
145+
'html',
146+
'htm',
147+
'css',
148+
'js',
149+
'ts',
150+
'tsx',
151+
'jsx',
152+
'md',
153+
'mdx',
154+
'csv',
155+
'pdf',
156+
'doc',
157+
'docx',
158+
'xls',
159+
'xlsx',
160+
'ppt',
161+
'pptx',
162+
'zip',
163+
'gz',
164+
'tar',
165+
'rar',
166+
'png',
167+
'jpg',
168+
'jpeg',
169+
'gif',
170+
'svg',
171+
'webp',
172+
'mp4',
173+
'mp3',
174+
'wav',
175+
'mov',
176+
'py',
177+
'rb',
178+
'go',
179+
'rs',
180+
'sh',
181+
'bat',
182+
'log',
183+
'env',
184+
])
185+
186+
function extractUrlInfo(text: string): { href: string; domain: string } | null {
187+
if (!text) return null
188+
if (/^https?:\/\//i.test(text)) {
189+
try {
190+
const url = new URL(text)
191+
return { href: text, domain: url.hostname }
192+
} catch {
193+
return null
194+
}
195+
}
196+
if (BARE_DOMAIN_RE.test(text)) {
197+
const tld = text.split('.').pop()?.toLowerCase() ?? ''
198+
if (FILE_EXTENSION_TLDS.has(tld)) return null
199+
return { href: `https://${text}`, domain: text }
200+
}
201+
return null
202+
}
203+
125204
interface CellRenderProps {
126205
kind: CellRenderKind
127206
/** When true the static content sits underneath the InlineEditor overlay
@@ -237,6 +316,31 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
237316
</span>
238317
)
239318

319+
case 'url':
320+
return (
321+
<span className={cn('flex min-w-0 items-center gap-1.5', isEditing && 'invisible')}>
322+
<img
323+
src={`https://www.google.com/s2/favicons?domain=${kind.domain}&sz=16`}
324+
alt=''
325+
width={12}
326+
height={12}
327+
className='shrink-0 rounded-[2px]'
328+
onError={(e) => {
329+
e.currentTarget.style.display = 'none'
330+
}}
331+
/>
332+
<a
333+
href={kind.href}
334+
target='_blank'
335+
rel='noopener noreferrer'
336+
className='min-w-0 overflow-clip text-ellipsis text-[var(--text-primary)] underline underline-offset-2 hover:opacity-70'
337+
onClick={(e) => e.stopPropagation()}
338+
>
339+
{kind.text}
340+
</a>
341+
</span>
342+
)
343+
240344
case 'text':
241345
return (
242346
<span

0 commit comments

Comments
 (0)