Skip to content

Commit d8b10aa

Browse files
committed
refactor(tables): clear the four blockers to moving the grid's view layer
Prep for relocating the table's presentational subtree to `components/resources/table-view/`. Each of these would have made that move either impossible or dishonest, so they land first, separately, with the tree still behaving identically. - `StatusBadge` moves to `components/execution-status/`. The tables grid drew it from `logs/utils`, which imports `getBlock` — so every table cell was pulling the whole block registry for a six-line badge. Three surfaces render it (logs list, log details, grid cells); none of them wanted that edge. - `sim-resource-cell` imported `ChatMessageContext` from the home route's re-export; it is defined in `components/chat/types`. Now read from the source. - `RemoteTableSelection` moves from the presence hook into the grid's own types, inverting the dependency: the overlay that draws a remote selection is presentational, the hook that produces it holds an authenticated socket. The overlay's two absolute self-imports become relative. - `CellContent` takes an `editor` slot instead of importing `InlineEditor`. `apps/sim` has no `sideEffects: false`, so the static import shipped the whole write path (and its authenticated `useTimezone`) regardless of `isEditing`. `workspaceId` also becomes optional — that is the security seam the `cell-render` test pins, and it was previously required, so nothing could exercise the undefined path a share surface needs. Interfaces consolidation from the playbook: - One `MODULE_RESOURCE_COPY` table for both authoring surfaces; `ResourcePickerField` now takes a `kind` instead of five copy props. The two pickers are deliberately NOT merged — one takes items as props, the other wires one query per branch so an unwired table module never fetches workflows. - `module-chrome.ts` now owns the interface grid geometry that was declared twice, including the placement custom properties, unified to `--module-row`/`--module-col`.
1 parent 56c33a3 commit d8b10aa

27 files changed

Lines changed: 372 additions & 268 deletions

File tree

apps/sim/app/workspace/[workspaceId]/interfaces/[interfaceId]/components/module-inspector/components/chat-module-fields/chat-module-fields.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -141,11 +141,7 @@ export function ChatModuleFields({
141141
return (
142142
<>
143143
<ResourcePickerField
144-
title='Workflow'
145-
missingMessage='This workflow is no longer in the workspace.'
146-
placeholder='Select a workflow'
147-
searchPlaceholder='Search workflows...'
148-
emptyMessage='No workflows in this workspace'
144+
kind='workflow'
149145
items={workflows.data}
150146
isLoading={workflows.isLoading}
151147
value={value.workflowId}

apps/sim/app/workspace/[workspaceId]/interfaces/[interfaceId]/components/module-inspector/components/form-module-fields/form-module-fields.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,11 +84,7 @@ export function FormModuleFields({
8484
return (
8585
<div className='flex flex-col'>
8686
<ResourcePickerField
87-
title='Workflow'
88-
missingMessage='This workflow is no longer in the workspace.'
89-
placeholder='Select a workflow'
90-
searchPlaceholder='Search workflows...'
91-
emptyMessage='No workflows in this workspace'
87+
kind='workflow'
9288
items={workflows.data}
9389
isLoading={workflows.isLoading}
9490
value={value.workflowId}

apps/sim/app/workspace/[workspaceId]/interfaces/[interfaceId]/components/module-inspector/components/resource-picker-field/resource-picker-field.test.tsx

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
import { act } from 'react'
55
import { createRoot, type Root } from 'react-dom/client'
66
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
7+
import type { ModuleResourceKind } from '@/components/resources/interface-view/module-resource-copy'
78
import { ResourcePickerField } from '@/app/workspace/[workspaceId]/interfaces/[interfaceId]/components/module-inspector/components/resource-picker-field/resource-picker-field'
89

910
let container: HTMLDivElement
@@ -20,16 +21,11 @@ afterEach(() => {
2021
container.remove()
2122
})
2223

23-
function render(title = 'Workflow') {
24+
function render(kind: ModuleResourceKind = 'workflow') {
2425
act(() => {
2526
root.render(
2627
<ResourcePickerField
27-
title={title}
28-
hint='Pick the workflow this module runs.'
29-
missingMessage='This workflow is no longer in the workspace.'
30-
placeholder='Select a workflow'
31-
searchPlaceholder='Search workflows...'
32-
emptyMessage='No workflows in this workspace'
28+
kind={kind}
3329
items={[{ id: 'wf-1', name: 'Onboarding' }]}
3430
isLoading={false}
3531
value='wf-1'
@@ -46,13 +42,13 @@ function combobox(): HTMLElement {
4642
}
4743

4844
describe('ResourcePickerField accessible names', () => {
49-
it('names the combobox from the field title', () => {
45+
it('names the combobox from the kind title', () => {
5046
render()
5147
expect(combobox()).toHaveAccessibleName('Workflow')
5248
})
5349

5450
it('puts the name on the combobox itself, not the layout wrapper', () => {
55-
render('Table')
51+
render('table')
5652
const named = Array.from(container.querySelectorAll<HTMLElement>('[aria-label]'))
5753
expect(named).toHaveLength(1)
5854
expect(named[0]).toBe(combobox())

apps/sim/app/workspace/[workspaceId]/interfaces/[interfaceId]/components/module-inspector/components/resource-picker-field/resource-picker-field.tsx

Lines changed: 21 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
import { useMemo } from 'react'
44
import { ChipCombobox, type ComboboxOption } from '@sim/emcn'
5+
import {
6+
MODULE_RESOURCE_COPY,
7+
type ModuleResourceKind,
8+
} from '@/components/resources/interface-view/module-resource-copy'
59
import { InspectorField } from '@/app/workspace/[workspaceId]/interfaces/[interfaceId]/components/inspector-field'
610

711
/** The minimal shape a pickable workspace resource exposes. */
@@ -11,13 +15,8 @@ export interface ResourcePickerItem {
1115
}
1216

1317
export interface ResourcePickerFieldProps {
14-
/** Field title; doubles as the combobox `aria-label`. */
15-
title: string
16-
/** Error shown when `value` no longer resolves against `items`. */
17-
missingMessage: string
18-
placeholder: string
19-
searchPlaceholder: string
20-
emptyMessage: string
18+
/** Which workspace resource is being bound; selects the field's copy. */
19+
kind: ModuleResourceKind
2120
/** Pickable resources — `undefined` while the list query resolves. */
2221
items: readonly ResourcePickerItem[] | undefined
2322
isLoading: boolean
@@ -34,24 +33,24 @@ export interface ResourcePickerFieldProps {
3433
* the chat/form workflow bindings).
3534
*
3635
* The picker gains a leading `None` entry once a resource is bound so the
37-
* binding can be cleared, and surfaces `missingMessage` when the bound id no
38-
* longer resolves — layout validation only guards writes, so a resource
39-
* deleted after wiring stays in the config until the user repoints it.
40-
* Options keep the list query's order, so every picker in the inspector
41-
* presents the same resources in the same order.
36+
* binding can be cleared, and surfaces the kind's missing-resource message
37+
* when the bound id no longer resolves — layout validation only guards writes,
38+
* so a resource deleted after wiring stays in the config until the user
39+
* repoints it. Options keep the list query's order, so every picker in the
40+
* inspector presents the same resources in the same order.
41+
*
42+
* Copy comes from `MODULE_RESOURCE_COPY`, shared with the in-canvas picker so
43+
* the inspector and the canvas never spell the same binding differently.
4244
*/
4345
export function ResourcePickerField({
44-
title,
45-
missingMessage,
46-
placeholder,
47-
searchPlaceholder,
48-
emptyMessage,
46+
kind,
4947
items,
5048
isLoading,
5149
value,
5250
onChange,
5351
disabled = false,
5452
}: ResourcePickerFieldProps) {
53+
const copy = MODULE_RESOURCE_COPY[kind]
5554
const options = useMemo<ComboboxOption[]>(() => {
5655
const list = (items ?? []).map((item) => ({ label: item.name, value: item.id }))
5756
if (!value) return list
@@ -61,19 +60,19 @@ export function ResourcePickerField({
6160
const missing = value !== null && items !== undefined && !items.some((item) => item.id === value)
6261

6362
return (
64-
<InspectorField title={title} error={missing ? missingMessage : undefined}>
63+
<InspectorField title={copy.title} error={missing ? copy.missingMessage : undefined}>
6564
<ChipCombobox
6665
options={options}
6766
value={value ?? ''}
6867
onChange={(next) => onChange(next === '' ? null : next)}
69-
placeholder={placeholder}
68+
placeholder={copy.placeholder}
7069
searchable
71-
searchPlaceholder={searchPlaceholder}
72-
emptyMessage={emptyMessage}
70+
searchPlaceholder={copy.searchPlaceholder}
71+
emptyMessage={copy.emptyMessage}
7372
isLoading={isLoading}
7473
disabled={disabled}
7574
maxHeight={260}
76-
aria-label={title}
75+
aria-label={copy.title}
7776
/>
7877
</InspectorField>
7978
)

apps/sim/app/workspace/[workspaceId]/interfaces/[interfaceId]/components/module-inspector/components/table-module-fields/table-module-fields.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -27,11 +27,7 @@ export function TableModuleFields({
2727

2828
return (
2929
<ResourcePickerField
30-
title='Table'
31-
missingMessage='This table is no longer in the workspace.'
32-
placeholder='Select a table'
33-
searchPlaceholder='Search tables...'
34-
emptyMessage='No tables in this workspace'
30+
kind='table'
3531
items={tables.data}
3632
isLoading={tables.isLoading}
3733
value={value.tableId}

apps/sim/app/workspace/[workspaceId]/interfaces/[interfaceId]/components/module-inspector/module-inspector.tsx

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -246,11 +246,7 @@ function FileModuleFields({
246246
const files = useWorkspaceFiles(workspaceId)
247247
return (
248248
<ResourcePickerField
249-
title='File'
250-
missingMessage='This file is no longer in the workspace.'
251-
placeholder='Select a file'
252-
searchPlaceholder='Search files...'
253-
emptyMessage='No files in this workspace'
249+
kind='file'
254250
items={files.data}
255251
isLoading={files.isLoading}
256252
value={value.fileId}

apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ArrowDown, ArrowUp, Check, ChevronUp, Clipboard, Search, X } from 'luci
2828
import { useParams, useRouter } from 'next/navigation'
2929
import { useQueryState } from 'nuqs'
3030
import { createPortal } from 'react-dom'
31+
import { getDisplayStatus, StatusBadge } from '@/components/execution-status'
3132
import type { WorkflowLogRow } from '@/lib/api/contracts/logs'
3233
import { BASE_EXECUTION_CHARGE } from '@/lib/billing/constants'
3334
import { apportionCredits, dollarsToCredits } from '@/lib/billing/credits/conversion'
@@ -49,8 +50,6 @@ import {
4950
import {
5051
DELETED_WORKFLOW_LABEL,
5152
formatDate,
52-
getDisplayStatus,
53-
StatusBadge,
5453
TriggerBadge,
5554
} from '@/app/workspace/[workspaceId]/logs/utils'
5655
import { useCodeViewerFeatures } from '@/hooks/use-code-viewer'

apps/sim/app/workspace/[workspaceId]/logs/logs.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,12 @@ import { formatDuration } from '@sim/utils/formatting'
2727
import { useQueryClient } from '@tanstack/react-query'
2828
import { useParams } from 'next/navigation'
2929
import { useQueryState } from 'nuqs'
30+
import {
31+
getDisplayStatus,
32+
type LogStatus,
33+
STATUS_CONFIG,
34+
StatusBadge,
35+
} from '@/components/execution-status'
3036
import type {
3137
WorkflowLogDetail,
3238
WorkflowLogRow,
@@ -92,11 +98,7 @@ import {
9298
DELETED_WORKFLOW_LABEL,
9399
extractRetryInput,
94100
formatDate,
95-
getDisplayStatus,
96-
type LogStatus,
97101
parseDuration,
98-
STATUS_CONFIG,
99-
StatusBadge,
100102
TriggerBadge,
101103
} from './utils'
102104

apps/sim/app/workspace/[workspaceId]/logs/utils.ts

Lines changed: 0 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -18,63 +18,6 @@ export const LOG_COLUMNS = {
1818

1919
export const DELETED_WORKFLOW_LABEL = 'Deleted Workflow'
2020

21-
export type LogStatus =
22-
| 'error'
23-
| 'pending'
24-
| 'running'
25-
| 'redacting'
26-
| 'info'
27-
| 'cancelled'
28-
| 'cancelling'
29-
30-
/**
31-
* Maps raw status string to LogStatus for display.
32-
* @param status - Raw status from API
33-
* @returns Normalized LogStatus value
34-
*/
35-
export function getDisplayStatus(status: string | null | undefined): LogStatus {
36-
switch (status) {
37-
case 'running':
38-
return 'running'
39-
case 'redacting':
40-
return 'redacting'
41-
case 'pending':
42-
return 'pending'
43-
case 'cancelling':
44-
return 'cancelling'
45-
case 'cancelled':
46-
return 'cancelled'
47-
case 'failed':
48-
return 'error'
49-
default:
50-
return 'info'
51-
}
52-
}
53-
54-
export const STATUS_CONFIG: Record<
55-
LogStatus,
56-
{
57-
variant: React.ComponentProps<typeof Badge>['variant']
58-
label: string
59-
color: string
60-
/** Whether this status appears as a filter option. Intermediary states (e.g. cancelling) are excluded. */
61-
filterable: boolean
62-
}
63-
> = {
64-
error: { variant: 'red', label: 'Error', color: 'var(--text-error)', filterable: true },
65-
pending: { variant: 'amber', label: 'Pending', color: '#f59e0b', filterable: true },
66-
running: { variant: 'amber', label: 'Running', color: '#f59e0b', filterable: true },
67-
redacting: { variant: 'amber', label: 'Redacting', color: '#f59e0b', filterable: false },
68-
cancelling: { variant: 'amber', label: 'Cancelling...', color: '#f59e0b', filterable: false },
69-
cancelled: { variant: 'orange', label: 'Cancelled', color: '#f97316', filterable: true },
70-
info: {
71-
variant: 'gray',
72-
label: 'Info',
73-
color: 'var(--terminal-status-info-color)',
74-
filterable: true,
75-
},
76-
}
77-
7821
const TRIGGER_VARIANT_MAP: Record<string, React.ComponentProps<typeof Badge>['variant']> = {
7922
manual: 'gray-secondary',
8023
api: 'blue',
@@ -89,24 +32,6 @@ const TRIGGER_VARIANT_MAP: Record<string, React.ComponentProps<typeof Badge>['va
8932
custom_block: 'blue-secondary',
9033
}
9134

92-
interface StatusBadgeProps {
93-
status: LogStatus
94-
}
95-
96-
/**
97-
* Renders a colored badge indicating log execution status.
98-
* @param props - Component props containing the status
99-
* @returns A Badge with dot indicator and status label
100-
*/
101-
export function StatusBadge({ status }: StatusBadgeProps) {
102-
const config = STATUS_CONFIG[status]
103-
return React.createElement(
104-
Badge,
105-
{ variant: config.variant, dot: true, size: 'sm' },
106-
config.label
107-
)
108-
}
109-
11035
interface TriggerBadgeProps {
11136
trigger: string
11237
}

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

Lines changed: 24 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,32 @@
11
'use client'
22

3+
import type { ReactNode } from 'react'
34
import type { RowExecutionMetadata } from '@/lib/table'
4-
import type { SaveReason } from '../../../types'
55
import type { DisplayColumn } from '../types'
66
import { CellRender, resolveCellRender } from './cell-render'
7-
import { InlineEditor } from './inline-editors'
87

98
interface CellContentProps {
109
value: unknown
1110
exec?: RowExecutionMetadata
1211
column: DisplayColumn
13-
/** Current workspace id — lets string cells holding an in-workspace resource
14-
* URL render as a tagged-resource chip instead of a plain external link. */
15-
workspaceId: string
12+
/**
13+
* Current workspace id — lets string cells holding an in-workspace resource URL
14+
* render as a tagged-resource chip instead of a plain external link.
15+
*
16+
* Optional, and that is the security seam: the chip's renderer mounts
17+
* workspace-authenticated list queries, so a surface with no workspace identity
18+
* (an anonymous share) passes `undefined` and the resolver never emits that kind.
19+
* See `cell-render.test.ts`.
20+
*/
21+
workspaceId?: string
1622
isEditing: boolean
17-
initialCharacter?: string | null
18-
onSave: (value: unknown, reason: SaveReason) => void
19-
onCancel: () => void
23+
/**
24+
* The editing surface, supplied by the host that owns the write path. Injected
25+
* rather than imported so a read-only surface never pulls the inline editor —
26+
* `apps/sim` has no `sideEffects: false`, so a static import would ship it
27+
* regardless of `isEditing`.
28+
*/
29+
editor?: ReactNode
2030
/**
2131
* Human-readable labels for unmet deps on this row+group, used to render a
2232
* "Waiting" pill when the cell hasn't run because something it depends on
@@ -29,19 +39,17 @@ interface CellContentProps {
2939

3040
/**
3141
* Glue layer: maps cell inputs to a typed `CellRenderKind` (via the pure
32-
* resolver) and renders the corresponding JSX (via the dumb renderer). The
33-
* inline editor sits on top when `isEditing` is true. Adding a new cell
34-
* appearance is a three-step mechanical change in the colocated files.
42+
* resolver) and renders the corresponding JSX (via the dumb renderer). The host's
43+
* `editor` sits on top when `isEditing` is true. Adding a new cell appearance is a
44+
* three-step mechanical change in the colocated files.
3545
*/
3646
export function CellContent({
3747
value,
3848
exec,
3949
column,
4050
workspaceId,
4151
isEditing,
42-
initialCharacter,
43-
onSave,
44-
onCancel,
52+
editor,
4553
waitingOnLabels,
4654
isEnrichmentOutput,
4755
}: CellContentProps) {
@@ -56,16 +64,8 @@ export function CellContent({
5664

5765
return (
5866
<>
59-
{isEditing && (
60-
<div className='absolute inset-0 z-10 flex items-center px-0'>
61-
<InlineEditor
62-
value={value}
63-
column={column}
64-
initialCharacter={initialCharacter ?? undefined}
65-
onSave={onSave}
66-
onCancel={onCancel}
67-
/>
68-
</div>
67+
{isEditing && editor && (
68+
<div className='absolute inset-0 z-10 flex items-center px-0'>{editor}</div>
6969
)}
7070
<CellRender kind={kind} isEditing={isEditing} />
7171
</>

0 commit comments

Comments
 (0)