Skip to content

Commit 31e85fb

Browse files
committed
fix(tables): use text input for number cells; idle poll backoff; csv error toast; column-cut drain
1 parent d4719f3 commit 31e85fb

3 files changed

Lines changed: 72 additions & 11 deletions

File tree

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

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -198,17 +198,13 @@ function InlineTextEditor({
198198
return (
199199
<input
200200
ref={inputRef}
201-
type={isNumber ? 'number' : 'text'}
202-
step={isNumber ? 'any' : undefined}
201+
type='text'
202+
inputMode={isNumber ? 'decimal' : undefined}
203203
value={draft ?? ''}
204204
onChange={(e) => setDraft(e.target.value)}
205205
onKeyDown={handleKeyDown}
206206
onBlur={() => doSave('blur')}
207-
className={cn(
208-
'w-full min-w-0 select-text border-none bg-transparent p-0 text-[var(--text-primary)] text-small outline-none',
209-
isNumber &&
210-
'[appearance:textfield] [&::-webkit-inner-spin-button]:appearance-none [&::-webkit-outer-spin-button]:appearance-none'
211-
)}
207+
className='w-full min-w-0 select-text border-none bg-transparent p-0 text-[var(--text-primary)] text-small outline-none'
212208
/>
213209
)
214210
}

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

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2078,6 +2078,65 @@ export function TableGrid({
20782078
if (!sel) return
20792079

20802080
e.preventDefault()
2081+
2082+
if (isColumnSelectionRef.current) {
2083+
// Column-header cut spans all rows — drain pages first, then use async
2084+
// clipboard so we don't block the event before the drain completes.
2085+
void (async () => {
2086+
const allRows = await ensureAllRowsLoadedRef.current()
2087+
const lines: string[] = []
2088+
const undoCells: Array<{ rowId: string; data: Record<string, unknown> }> = []
2089+
const batchUpdates: Array<{ rowId: string; data: Record<string, unknown> }> = []
2090+
for (const row of allRows) {
2091+
const cells: string[] = []
2092+
const updates: Record<string, unknown> = {}
2093+
const previousData: Record<string, unknown> = {}
2094+
for (let c = sel.startCol; c <= sel.endCol; c++) {
2095+
const colName = cols[c]?.name
2096+
if (!colName) continue
2097+
const value: unknown = row.data[colName]
2098+
cells.push(
2099+
value === null || value === undefined
2100+
? ''
2101+
: typeof value === 'object'
2102+
? JSON.stringify(value)
2103+
: String(value)
2104+
)
2105+
previousData[colName] = row.data[colName] ?? null
2106+
updates[colName] = null
2107+
}
2108+
lines.push(cells.join('\t'))
2109+
undoCells.push({ rowId: row.id, data: previousData })
2110+
batchUpdates.push({ rowId: row.id, data: updates })
2111+
}
2112+
if (!navigator.clipboard) {
2113+
toast.error('Clipboard access is unavailable in this context')
2114+
return
2115+
}
2116+
try {
2117+
await navigator.clipboard.writeText(lines.join('\n'))
2118+
} catch (err) {
2119+
if (err instanceof DOMException && err.name === 'NotAllowedError') {
2120+
toast.error(
2121+
'Clipboard permission expired — press Cmd+X again immediately after selecting'
2122+
)
2123+
return
2124+
}
2125+
throw err
2126+
}
2127+
if (undoCells.length > 0) {
2128+
pushUndoRef.current({ type: 'clear-cells', cells: undoCells })
2129+
}
2130+
if (batchUpdates.length > 0) {
2131+
await chunkBatchUpdates(batchUpdates, batchUpdateAsyncRef.current)
2132+
}
2133+
})().catch((error) => {
2134+
logger.error('Failed to cut column cells', { error })
2135+
toast.error('Failed to cut — please try again')
2136+
})
2137+
return
2138+
}
2139+
20812140
const lines: string[] = []
20822141
const undoCells: Array<{ rowId: string; data: Record<string, unknown> }> = []
20832142
const batchUpdates: Array<{ rowId: string; data: Record<string, unknown> }> = []

apps/sim/hooks/queries/tables.ts

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ import {
7979
const logger = createLogger('TableQueries')
8080

8181
const ROWS_POLL_INTERVAL_WHILE_RUNNING_MS = 2_000
82+
const ROWS_POLL_INTERVAL_IDLE_MS = 30_000
8283

8384
type TableQueryScope = 'active' | 'archived' | 'all'
8485

@@ -341,6 +342,7 @@ export function useInfiniteTableRows({
341342
let timeoutId: ReturnType<typeof setTimeout> | null = null
342343
const tick = async () => {
343344
if (cancelled) return
345+
let hasDirty = false
344346
if (queryClient.isMutating() === 0) {
345347
const data = queryClient.getQueryData<InfiniteData<TableRowsResponse, number>>(queryKey)
346348
const dirty: number[] = []
@@ -351,7 +353,8 @@ export function useInfiniteTableRows({
351353
}
352354
}
353355
}
354-
if (dirty.length > 0) {
356+
hasDirty = dirty.length > 0
357+
if (hasDirty) {
355358
await Promise.all(
356359
dirty.map(async (offset) => {
357360
try {
@@ -388,8 +391,12 @@ export function useInfiniteTableRows({
388391
if (cancelled) return
389392
// Recursive setTimeout instead of setInterval so a slow tick can't
390393
// overlap the next one — out-of-order responses would otherwise let
391-
// stale data overwrite fresh.
392-
timeoutId = setTimeout(() => void tick(), ROWS_POLL_INTERVAL_WHILE_RUNNING_MS)
394+
// stale data overwrite fresh. Use a long interval when idle so tables
395+
// with no running executions don't burn CPU on constant cache reads.
396+
timeoutId = setTimeout(
397+
() => void tick(),
398+
hasDirty ? ROWS_POLL_INTERVAL_WHILE_RUNNING_MS : ROWS_POLL_INTERVAL_IDLE_MS
399+
)
393400
}
394401
timeoutId = setTimeout(() => void tick(), ROWS_POLL_INTERVAL_WHILE_RUNNING_MS)
395402
return () => {
@@ -1036,7 +1043,6 @@ export function useUploadCsvToTable() {
10361043
return response.json()
10371044
},
10381045
onError: (error) => {
1039-
if (isValidationError(error)) return
10401046
logger.error('Failed to upload CSV:', error)
10411047
toast.error(error.message, { duration: 5000 })
10421048
},

0 commit comments

Comments
 (0)