Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
c36f80c
fix(tables): fix bulk ops truncation for tables larger than one page
waleedlatif1 May 9, 2026
9099a98
chore(tables): remove extraneous comments
waleedlatif1 May 9, 2026
61752e9
fix(tables): add missing useEffect import; chunk range-selection dele…
waleedlatif1 May 9, 2026
2cb026d
fix(tables): add hasRunningGroupExecution and import it
waleedlatif1 May 9, 2026
a4e7db2
fix(tables): define mergePagePreservingIdentity helper used in pollin…
waleedlatif1 May 9, 2026
46e3bbf
fix(tables): define ROWS_POLL_INTERVAL_WHILE_RUNNING_MS constant used…
waleedlatif1 May 9, 2026
6f93d34
fix(tables): capture rowSel before await in delete handler; handle cl…
waleedlatif1 May 9, 2026
8342607
fix(tables): abort cut on clipboard NotAllowedError to prevent silent…
waleedlatif1 May 9, 2026
691746d
fix(tables): push undo before chunkBatchUpdates to survive partial ch…
waleedlatif1 May 9, 2026
fa77b7f
fix(tables): use text input for number cells; idle poll backoff; csv …
waleedlatif1 May 9, 2026
4c63d4f
fix(tables): audit fixes — column-copy drain, polling scope, merge id…
waleedlatif1 May 9, 2026
783d397
improvement(tables): cleanup — extract components, stabilize callback…
waleedlatif1 May 9, 2026
e3626f1
improvement(tables): remove polling, eager drain, and parallelize bat…
waleedlatif1 May 9, 2026
e8b3c95
chore(tables): remove stale comments and dead defensive code
waleedlatif1 May 9, 2026
3f0da17
fix(tables): run-all selection sends all rows to server, not just loa…
waleedlatif1 May 9, 2026
bf98e8c
test(tables): remove background-drain describe block (behavior intent…
waleedlatif1 May 9, 2026
e3f2da4
fix(tables): surface CSV import error toast; remove dead hasRunningGr…
waleedlatif1 May 9, 2026
e7b5a01
fix(tables): make runWithoutRecording async-aware so undoRedoInProgre…
waleedlatif1 May 9, 2026
685ae54
feat(tables): render URL cells with favicon and clickable link
waleedlatif1 May 9, 2026
290b4bf
feat(tables): clickable URL cells with favicons using tldts
waleedlatif1 May 9, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion apps/sim/app/api/table/import-csv/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
inferSchemaFromCsv,
parseCsvBuffer,
sanitizeName,
TABLE_LIMITS,
type TableSchema,
} from '@/lib/table'
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
Expand Down Expand Up @@ -67,7 +68,10 @@ export const POST = withRouteHandler(async (request: NextRequest) => {
const { headers, rows } = await parseCsvBuffer(buffer, delimiter)

const { columns, headerToColumn } = inferSchemaFromCsv(headers, rows)
const tableName = sanitizeName(file.name.replace(/\.[^.]+$/, ''), 'imported_table')
const tableName = sanitizeName(file.name.replace(/\.[^.]+$/, ''), 'imported_table').slice(
0,
TABLE_LIMITS.MAX_TABLE_NAME_LENGTH
)
const planLimits = await getWorkspaceTableLimits(workspaceId)

const normalizedSchema: TableSchema = {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,29 +1,14 @@
'use client'

import type React from 'react'
import { parse } from 'tldts'
import { Badge, Checkbox, Tooltip } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import type { RowExecutionMetadata } from '@/lib/table'
import { StatusBadge } from '@/app/workspace/[workspaceId]/logs/utils'
import { storageToDisplay } from '../../../utils'
import type { DisplayColumn } from '../types'

/**
* Discriminated union describing every shape a table cell can take.
*
* Workflow-output cells follow a status state machine: they always render
* *something* (a value, a status pill, or a dash), driven by the combination
* of `executions[groupId]` state and dep satisfaction. Plain (non-workflow)
* cells just render the typed value or empty.
*
* `'empty'` is the universal fallback used by both workflow cells (no exec,
* no value, no waiting) and plain cells (null/undefined value).
*
* Adding a new cell appearance is a three-step mechanical change: add a
* variant here, pick it in `resolveCellRender`, render it in `CellRender`.
* TypeScript's exhaustiveness check on the renderer's `switch` (the
* unreachable default) flags any branch you forgot.
*/
export type CellRenderKind =
// Workflow-output cells
| { kind: 'value'; text: string }
Expand All @@ -38,6 +23,7 @@ export type CellRenderKind =
| { kind: 'boolean'; checked: boolean }
| { kind: 'json'; text: string }
| { kind: 'date'; text: string }
| { kind: 'url'; text: string; href: string; domain: string }
| { kind: 'text'; text: string }
// Universal fallback
| { kind: 'empty' }
Expand All @@ -46,20 +32,9 @@ interface ResolveCellRenderInput {
value: unknown
exec: RowExecutionMetadata | undefined
column: DisplayColumn
/** Empty / undefined → not waiting; non-empty → render the Waiting pill. */
waitingOnLabels: string[] | undefined
}

/**
* Decide which `CellRenderKind` to render for a cell. Pure — easily
* unit-testable in isolation, no JSX involved.
*
* Order matters for workflow cells: block-error wins over a value (the user
* cares about the failure), value wins over running/queued (we have data
* already), and the running/queued branch deliberately collapses pre-enqueue
* `pending` and post-enqueue `queued` into one `Queued` pill so the cell
* doesn't flicker as the row transitions from one to the other.
*/
export function resolveCellRender({
value,
exec,
Expand All @@ -76,31 +51,20 @@ export function resolveCellRender({

if (blockError) return { kind: 'block-error' }

// Active re-run of THIS column wins over its prior value — the value is
// about to be overwritten and the user should see the cell is changing.
const inFlight =
exec?.status === 'running' || exec?.status === 'queued' || exec?.status === 'pending'
if (inFlight && blockRunning) return { kind: 'running' }

// Value wins over `pending-upstream`: once this column's output has
// landed, the cell is done from the user's perspective — even if the
// group is still running other blocks downstream. Without this, mid-run
// partial-write events (`status: 'running'` carrying outputs but tagging
// a different block as running) would flip a finished column back to the
// amber Pending pill until the terminal `completed` event arrives.
// Value wins over pending-upstream: a finished column stays finished even
// while other blocks in the group are still running.
if (!isNull) return { kind: 'value', text: stringifyValue(value) }

if (inFlight && !(groupHasBlockErrors && !blockRunning)) {
if (exec?.status === 'queued' || exec?.status === 'pending') return { kind: 'queued' }
// `running` with this block not in `runningBlockIds` and no value yet =
// upstream block still going; surface as the amber Pending pill.
return { kind: 'pending-upstream' }
}

// Waiting wins over a stale terminal state: if deps are unmet right now,
// the prior `cancelled` / `error` is informational at best — the cell
// can't actually run until the user fills the missing input. Surface the
// actionable state instead of the stale one.
// Waiting wins over a stale terminal status — show the actionable state.
if (waitingOnLabels && waitingOnLabels.length > 0) {
return { kind: 'waiting', labels: waitingOnLabels }
}
Expand All @@ -113,6 +77,12 @@ export function resolveCellRender({
if (isNull) return { kind: 'empty' }
if (column.type === 'json') return { kind: 'json', text: JSON.stringify(value) }
if (column.type === 'date') return { kind: 'date', text: String(value) }
if (column.type === 'string') {
const text = stringifyValue(value)
const urlInfo = extractUrlInfo(text)
if (urlInfo) return { kind: 'url', text, href: urlInfo.href, domain: urlInfo.domain }
return { kind: 'text', text }
}
return { kind: 'text', text: stringifyValue(value) }
}

Expand All @@ -122,19 +92,32 @@ function stringifyValue(value: unknown): string {
return JSON.stringify(value)
}

const BARE_DOMAIN_RE = /^([a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?\.)+[a-zA-Z]{2,}$/

function extractUrlInfo(text: string): { href: string; domain: string } | null {
const trimmed = text.trim()
if (!trimmed) return null
if (/^https?:\/\//i.test(trimmed)) {
try {
const url = new URL(trimmed)
return { href: trimmed, domain: url.hostname }
} catch {
return null
}
}
if (BARE_DOMAIN_RE.test(trimmed)) {
const parsed = parse(trimmed)
if (!parsed.isIcann) return null
return { href: `https://${trimmed}`, domain: trimmed }
}
return null
}

interface CellRenderProps {
kind: CellRenderKind
/** When true the static content sits underneath the InlineEditor overlay
* and should be visually hidden (but kept in flow to preserve cell size). */
isEditing: boolean
}

/**
* Pure renderer: takes a `CellRenderKind` and returns the JSX. No business
* logic — adding a new cell appearance means adding a new `case` here. The
* exhaustiveness check on the `switch` (the unreachable default) flags any
* variant you forgot to handle.
*/
export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactElement | null {
switch (kind.kind) {
case 'value':
Expand Down Expand Up @@ -237,6 +220,35 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
</span>
)

case 'url':
return (
<span className={cn('flex min-w-0 items-center gap-1.5', isEditing && 'invisible')}>
<img
src={`https://www.google.com/s2/favicons?domain=${encodeURIComponent(kind.domain)}&sz=16`}
alt=''
width={12}
height={12}
className='shrink-0 rounded-[2px]'
onError={(e) => {
e.currentTarget.style.display = 'none'
}}
/>
<a
href={kind.href}
target='_blank'
rel='noopener noreferrer'
className={cn(
'min-w-0 overflow-clip text-ellipsis text-[var(--text-primary)] underline underline-offset-2 hover:opacity-70',
isEditing && 'pointer-events-none'
)}
onClick={(e) => e.stopPropagation()}
onDoubleClick={(e) => e.stopPropagation()}
>
{kind.text}
</a>
</span>
)

case 'text':
return (
<span
Expand All @@ -253,20 +265,12 @@ export function CellRender({ kind, isEditing }: CellRenderProps): React.ReactEle
return null

default: {
// Exhaustiveness guard: TypeScript flags this branch if a new
// `CellRenderKind` variant is added without a matching `case` above.
const _exhaustive: never = kind
return _exhaustive
}
}
}

/**
* Workflow-output cells are hand-editable; while editing, the static content
* must stay in flow (so the cell doesn't collapse) but be visually hidden so
* the InlineEditor overlay shows through. Plain wrapper around any non-text
* variant.
*/
function Wrap({ isEditing, children }: { isEditing: boolean; children: React.ReactNode }) {
if (!isEditing) return <>{children}</>
return <div className='invisible'>{children}</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,11 @@ const EXPANDED_CELL_MIN_WIDTH = 420
const EXPANDED_CELL_HEIGHT = 280

/**
* Supabase-style anchored cell expander. Floats over the clicked cell at the cell's
* top-left, minimum width {@link EXPANDED_CELL_MIN_WIDTH}, fixed height, internally
* scrollable. Triggered by cell double-click so long values are readable/editable
* without widening the column. Inline edit via Enter/F2/typing is unaffected.
* Anchored cell editor. Floats over the double-clicked cell, minimum width
* {@link EXPANDED_CELL_MIN_WIDTH}, fixed height, internally scrollable.
*
* Workflow and boolean cells are read-only in this view — workflow cells are driven
* by the scheduler, booleans use a checkbox cell inline.
* Workflow and boolean cells are read-only here — workflow cells are driven
* by the scheduler, booleans toggle inline.
*/
export function ExpandedCellPopover({
expandedCell,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ function InlineDateEditor({
)
}

/** Inline editor for `string`/`number`/`json` columns — single-line text input. */
/** Inline editor for `string`/`number`/`json` columns — single-line text input. Number columns use `type="number"` so the browser rejects non-numeric input. */
function InlineTextEditor({
value,
column,
Expand Down Expand Up @@ -193,17 +193,18 @@ function InlineTextEditor({
}
}

const isNumber = column.type === 'number'

return (
<input
ref={inputRef}
type='text'
value={draft}
inputMode={isNumber ? 'decimal' : undefined}
value={draft ?? ''}
onChange={(e) => setDraft(e.target.value)}
Comment thread
waleedlatif1 marked this conversation as resolved.
onKeyDown={handleKeyDown}
onBlur={() => doSave('blur')}
className={cn(
'w-full min-w-0 select-text border-none bg-transparent p-0 text-[var(--text-primary)] text-small outline-none'
)}
className='w-full min-w-0 select-text border-none bg-transparent p-0 text-[var(--text-primary)] text-small outline-none'
/>
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,24 @@ export const SELECTION_TINT_BG = 'bg-[rgba(37,99,235,0.06)]'
* been measured yet and as the initial width for newly-added columns. */
export const COL_WIDTH = 160

/** Width of the "add column" placeholder column in pixels. */
export const ADD_COL_WIDTH = 120

/** Column config sidebar width in pixels — drives both the sidebar's own width
* and the table's reserved padding-right while a sidebar is open. */
export const COLUMN_SIDEBAR_WIDTH = 400

/** Number of skeleton rows shown while the table body is loading. */
export const SKELETON_ROW_COUNT = 10

export const CELL =
'border-[var(--border)] border-r border-b px-2 py-[7px] align-middle select-none'
export const CELL_CHECKBOX =
'sticky left-0 z-[6] border-[var(--border)] border-r border-b bg-[var(--bg)] px-1 py-[7px] align-middle select-none'
export const CELL_HEADER_CHECKBOX =
'sticky left-0 z-[12] border-[var(--border)] border-r border-b bg-[var(--bg)] px-1 py-[7px] text-center align-middle'
/** Fixed height (not min-) so a Badge-rendered status pill doesn't make the row grow vs a plain-text neighbor. */
export const CELL_CONTENT =
'relative flex h-[22px] min-w-0 items-center overflow-clip text-ellipsis whitespace-nowrap text-small'
export const SELECTION_OVERLAY =
'pointer-events-none absolute -top-px -right-px -bottom-px z-[5] border-[2px] border-[var(--selection)]'
Loading
Loading