Skip to content

Commit 44688c4

Browse files
Column sidebar improvements
1 parent 3b1ef90 commit 44688c4

3 files changed

Lines changed: 305 additions & 84 deletions

File tree

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/column-sidebar/column-sidebar.tsx

Lines changed: 187 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,15 @@ import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
2525
import type { InputFormatField } from '@/lib/workflows/types'
2626
import { getBlock } from '@/blocks'
2727
import { PreviewWorkflow } from '@/app/workspace/[workspaceId]/w/components/preview'
28+
import { useDeploymentInfo, useDeployWorkflow } from '@/hooks/queries/deployments'
2829
import {
2930
useAddTableColumn,
3031
useAddWorkflowGroup,
3132
useUpdateColumn,
3233
useUpdateWorkflowGroup,
3334
} from '@/hooks/queries/tables'
3435
import { useWorkflowState, workflowKeys } from '@/hooks/queries/workflows'
36+
import { useUserPermissionsContext } from '@/app/workspace/[workspaceId]/providers/workspace-permissions-provider'
3537
import type { WorkflowMetadata } from '@/stores/workflows/registry/types'
3638
import { COLUMN_TYPE_OPTIONS, type SidebarColumnType } from './column-types'
3739

@@ -224,6 +226,44 @@ function FieldError({ message }: { message: string }) {
224226
return <p className='pl-0.5 text-destructive text-caption'>{message}</p>
225227
}
226228

229+
/**
230+
* Tinted inline warning row with a message on the left and an action button
231+
* on the right. Stacks naturally — render multiple in sequence and they line
232+
* up. Color mirrors the group-header deploy badge: `red` for blocking states,
233+
* `amber` for soft warnings.
234+
*/
235+
function WarningRow({
236+
tone,
237+
message,
238+
action,
239+
}: {
240+
tone: 'red' | 'amber'
241+
message: string
242+
action: React.ReactNode
243+
}) {
244+
return (
245+
<div
246+
className={cn(
247+
'flex items-center gap-2 rounded-md border px-2.5 py-2',
248+
tone === 'red'
249+
? 'border-destructive/40 bg-destructive/5'
250+
: 'border-amber-500/40 bg-amber-500/5'
251+
)}
252+
role='status'
253+
>
254+
<span
255+
className={cn(
256+
'min-w-0 flex-1 text-caption',
257+
tone === 'red' ? 'text-destructive' : 'text-amber-700 dark:text-amber-400'
258+
)}
259+
>
260+
{message}
261+
</span>
262+
<div className='shrink-0'>{action}</div>
263+
</div>
264+
)
265+
}
266+
227267
/**
228268
* Right-edge configuration panel for any column.
229269
*
@@ -274,6 +314,15 @@ export function ColumnSidebar({
274314
const isWorkflow =
275315
!!existingGroup || configState?.mode === 'new' || typeInput === 'workflow'
276316

317+
/**
318+
* Show the Column name field whenever a *specific* column is open: scalar
319+
* columns (create or edit) and per-output workflow columns (edit only). Hide
320+
* it when the surface is the workflow-group as a whole — i.e. creating a
321+
* brand-new workflow column where individual output names are auto-derived.
322+
*/
323+
const showColumnNameField =
324+
!isWorkflow || configState?.mode === 'edit' || configState?.mode === 'new'
325+
277326
/**
278327
* Columns to the left of the current column — these are the only valid trigger
279328
* dependencies, since a workflow column can't depend on values that haven't been
@@ -413,6 +462,26 @@ export function ColumnSidebar({
413462
open && isWorkflow && selectedWorkflowId ? selectedWorkflowId : undefined
414463
)
415464

465+
/**
466+
* Deployment status of the picked workflow. The sidebar surfaces an inline
467+
* warning row offering one-click (re)deploy, mirroring the badge in the
468+
* group's column header so the user sees the same signal where they're
469+
* configuring the column.
470+
*/
471+
const deploymentInfo = useDeploymentInfo(
472+
open && isWorkflow && selectedWorkflowId ? selectedWorkflowId : null,
473+
{ refetchOnMount: 'always' }
474+
)
475+
const deployState: 'undeployed' | 'redeploy' | null = deploymentInfo.data
476+
? !deploymentInfo.data.isDeployed
477+
? 'undeployed'
478+
: deploymentInfo.data.needsRedeployment
479+
? 'redeploy'
480+
: null
481+
: null
482+
const { mutate: deployWorkflow, isPending: isDeploying } = useDeployWorkflow()
483+
const userPermissions = useUserPermissionsContext()
484+
416485
/**
417486
* Resolves the unified Start block id and its current `inputFormat` field
418487
* names. The "Add inputs" mutation only adds rows for table columns that
@@ -672,7 +741,13 @@ export function ColumnSidebar({
672741
if (!configState) return
673742
setSaveError(null)
674743
const trimmedName = nameInput.trim()
675-
if (!trimmedName || (isWorkflow && (!selectedWorkflowId || selectedOutputs.length === 0))) {
744+
// Name is required iff the field is shown — when configuring a whole
745+
// workflow group at creation time, per-output column names are auto-derived
746+
// and the field is hidden, so don't gate save on it.
747+
if (
748+
(showColumnNameField && !trimmedName) ||
749+
(isWorkflow && (!selectedWorkflowId || selectedOutputs.length === 0))
750+
) {
676751
setShowValidation(true)
677752
return
678753
}
@@ -688,8 +763,23 @@ export function ColumnSidebar({
688763
if (existingGroup) {
689764
// Update path: diff outputs, derive new column names for added entries,
690765
// call updateWorkflowGroup so service handles add/remove transactionally.
766+
// If the sidebar was opened on a *specific* workflow-output column and
767+
// the user renamed it, propagate that into the group's `outputs` ref
768+
// (the column rename itself goes through `updateColumn` below, which
769+
// server-side cascades into outputs/deps — but our outgoing payload
770+
// also has to use the new name so the group update doesn't undo it).
771+
const editedColumnName =
772+
configState.mode === 'edit' ? configState.columnName : null
773+
const renamedColumn =
774+
editedColumnName && trimmedName && trimmedName !== editedColumnName
775+
? { from: editedColumnName, to: trimmedName }
776+
: null
691777
const oldKeys = new Set(existingGroup.outputs.map((o) => `${o.blockId}::${o.path}`))
692-
const taken = new Set(allColumns.map((c) => c.name))
778+
const taken = new Set(
779+
allColumns.map((c) =>
780+
renamedColumn && c.name === renamedColumn.from ? renamedColumn.to : c.name
781+
)
782+
)
693783
const fullOutputs: WorkflowGroupOutput[] = []
694784
const newOutputColumns: ColumnDefinition[] = []
695785
for (const o of orderedOutputs) {
@@ -698,7 +788,11 @@ export function ColumnSidebar({
698788
(e) => e.blockId === o.blockId && e.path === o.path
699789
)
700790
if (existing) {
701-
fullOutputs.push(existing)
791+
fullOutputs.push(
792+
renamedColumn && existing.columnName === renamedColumn.from
793+
? { ...existing, columnName: renamedColumn.to }
794+
: existing
795+
)
702796
} else {
703797
const blockName = blockNameByBlockId.get(o.blockId) ?? 'output'
704798
const colName = deriveOutputColumnName(blockName, o.path, taken)
@@ -714,6 +808,12 @@ export function ColumnSidebar({
714808
}
715809
oldKeys.delete(key)
716810
}
811+
if (renamedColumn) {
812+
await updateColumn.mutateAsync({
813+
columnName: renamedColumn.from,
814+
updates: { name: renamedColumn.to },
815+
})
816+
}
717817
await updateWorkflowGroup.mutateAsync({
718818
groupId: existingGroup.id,
719819
workflowId: selectedWorkflowId,
@@ -835,24 +935,28 @@ export function ColumnSidebar({
835935
/>
836936
</div>
837937

838-
<FieldDivider />
938+
{showColumnNameField && (
939+
<>
940+
<FieldDivider />
839941

840-
<div className='flex flex-col gap-[9.5px]'>
841-
<FieldLabel htmlFor='column-sidebar-name' required>
842-
Column name
843-
</FieldLabel>
844-
<Input
845-
id='column-sidebar-name'
846-
value={nameInput}
847-
onChange={(e) => setNameInput(e.target.value)}
848-
spellCheck={false}
849-
autoComplete='off'
850-
aria-invalid={showValidation && !nameInput.trim() ? true : undefined}
851-
/>
852-
{showValidation && !nameInput.trim() && (
853-
<FieldError message='Column name is required' />
854-
)}
855-
</div>
942+
<div className='flex flex-col gap-[9.5px]'>
943+
<FieldLabel htmlFor='column-sidebar-name' required>
944+
Column name
945+
</FieldLabel>
946+
<Input
947+
id='column-sidebar-name'
948+
value={nameInput}
949+
onChange={(e) => setNameInput(e.target.value)}
950+
spellCheck={false}
951+
autoComplete='off'
952+
aria-invalid={showValidation && !nameInput.trim() ? true : undefined}
953+
/>
954+
{showValidation && !nameInput.trim() && (
955+
<FieldError message='Column name is required' />
956+
)}
957+
</div>
958+
</>
959+
)}
856960

857961
{!isWorkflow && (
858962
<>
@@ -955,27 +1059,70 @@ export function ColumnSidebar({
9551059
{selectedWorkflowId &&
9561060
startBlockInputs.blockId &&
9571061
missingInputColumnNames.length > 0 && (
958-
<Tooltip.Root>
959-
<Tooltip.Trigger asChild>
960-
<Button
961-
type='button'
962-
variant='default'
963-
size='sm'
964-
onClick={() => addInputsMutation.mutate()}
965-
disabled={addInputsMutation.isPending}
966-
className='self-start'
967-
>
968-
<Plus className='h-[14px] w-[14px]' />
969-
{addInputsMutation.isPending
970-
? 'Adding…'
971-
: `Add inputs (${missingInputColumnNames.length})`}
972-
</Button>
973-
</Tooltip.Trigger>
974-
<Tooltip.Content side='top'>
975-
Adds {missingInputColumnNames.join(', ')} to the workflow's Start block
976-
</Tooltip.Content>
977-
</Tooltip.Root>
1062+
<WarningRow
1063+
tone='amber'
1064+
message={`Start block missing ${missingInputColumnNames.length} table input${
1065+
missingInputColumnNames.length === 1 ? '' : 's'
1066+
}`}
1067+
action={
1068+
<Tooltip.Root>
1069+
<Tooltip.Trigger asChild>
1070+
<Button
1071+
type='button'
1072+
variant='default'
1073+
size='sm'
1074+
onClick={() => addInputsMutation.mutate()}
1075+
disabled={addInputsMutation.isPending}
1076+
>
1077+
<Plus className='h-[14px] w-[14px]' />
1078+
{addInputsMutation.isPending
1079+
? 'Adding…'
1080+
: `Add inputs (${missingInputColumnNames.length})`}
1081+
</Button>
1082+
</Tooltip.Trigger>
1083+
<Tooltip.Content side='top'>
1084+
Adds {missingInputColumnNames.join(', ')} to the workflow's Start block
1085+
</Tooltip.Content>
1086+
</Tooltip.Root>
1087+
}
1088+
/>
9781089
)}
1090+
{selectedWorkflowId && deployState && (
1091+
<WarningRow
1092+
tone={deployState === 'undeployed' ? 'red' : 'amber'}
1093+
message={
1094+
deployState === 'undeployed'
1095+
? 'Workflow is not deployed'
1096+
: 'Workflow has changes since last deploy'
1097+
}
1098+
action={
1099+
<Tooltip.Root>
1100+
<Tooltip.Trigger asChild>
1101+
<Button
1102+
type='button'
1103+
variant='default'
1104+
size='sm'
1105+
onClick={() => deployWorkflow({ workflowId: selectedWorkflowId })}
1106+
disabled={isDeploying || !userPermissions.canAdmin}
1107+
>
1108+
{isDeploying
1109+
? 'Deploying…'
1110+
: deployState === 'undeployed'
1111+
? 'Deploy'
1112+
: 'Redeploy'}
1113+
</Button>
1114+
</Tooltip.Trigger>
1115+
<Tooltip.Content side='top'>
1116+
{!userPermissions.canAdmin
1117+
? 'Admin permission required to deploy'
1118+
: deployState === 'undeployed'
1119+
? 'Deploy this workflow'
1120+
: 'Redeploy with the latest changes'}
1121+
</Tooltip.Content>
1122+
</Tooltip.Root>
1123+
}
1124+
/>
1125+
)}
9791126
</div>
9801127

9811128
<FieldDivider />

0 commit comments

Comments
 (0)