@@ -56,7 +56,7 @@ import { extractCreatedRowId, useTableUndo } from '@/hooks/use-table-undo'
5656import type { ChatContext } from '@/stores/panel'
5757import type { DeletedRowSnapshot } from '@/stores/table/types'
5858import { useContextMenu , useTable } from '../../hooks'
59- import type { EditingCell , QueryOptions , SaveReason } from '../../types'
59+ import type { EditingCell , QueryOptions , RowInsertTarget , SaveReason } from '../../types'
6060import { cleanCellValue , generateColumnName as sharedGenerateColumnName } from '../../utils'
6161import type { ColumnConfig } from '../column-config-sidebar'
6262import { ColumnDropdown } from '../column-dropdown'
@@ -209,8 +209,8 @@ interface TableGridProps {
209209 onOpenEnrichmentDetails : ( rowId : string , groupId : string ) => void
210210 /** Open the row-edit modal for `row`. Wrapper renders the modal. */
211211 onOpenRowModal : ( row : TableRowType ) => void
212- /** Opens the add-row form, which inserts a complete row in one request . */
213- onOpenAddRowModal : ( ) => void
212+ /** Opens the add-row form, which inserts a complete row at `insertAt` (appends when omitted) . */
213+ onOpenAddRowModal : ( insertAt ?: RowInsertTarget ) => void
214214 /** Open the row-delete modal for `snapshots`. Wrapper renders the modal. */
215215 onRequestDeleteRows : ( snapshots : DeletedRowSnapshot [ ] ) => void
216216 /**
@@ -373,6 +373,15 @@ function writeLoadedRowsWithChip(opts: {
373373 return true
374374}
375375
376+ /**
377+ * Whether new rows must go through the add-row form instead of a blank grid row.
378+ * A blank row only works when the grid can fill it in afterwards: typing into it
379+ * is an update, and the server rejects an empty row when any column is required.
380+ */
381+ function needsAddRowForm ( updateLocked : boolean | undefined , columns : ColumnDefinition [ ] ) : boolean {
382+ return Boolean ( updateLocked ) || columns . some ( ( column ) => column . required )
383+ }
384+
376385/**
377386 * Value-equality for a cell's stored value vs a pending edit. Primitives compare
378387 * with `===`; arrays/objects (multiselect id arrays, json) compare structurally
@@ -703,15 +712,14 @@ export function TableGrid({
703712 // requires the delete lock clear too — mirror that here or the affordance
704713 // stays live on an append-only table and only fails on click.
705714 const canDestroyColumn = canMutateSchema && ! locks ?. deleteLocked
706- // Duplicate inserts a full copied row in one shot, so unlike the blank-row
707- // paths it needs the insert lock only — it is valid on an append-only table.
715+ /**
716+ * Inserts that carry the whole row in one request (Duplicate, paste-append, the
717+ * add-row form) need only the insert lock, so they stay valid on an append-only
718+ * table. New row, Shift+Enter, and Insert row fall back to that form whenever
719+ * `needsAddRowForm` says a blank row can't work.
720+ */
708721 const canInsertFullRow = userPermissions . canEdit && ! locks ?. insertLocked
709- // Manual grid entry is "add an empty row, then type into its cells" — the
710- // typing is an update. So a *useful* manual add needs BOTH insert and update
711- // unlocked; on an append-only table (update locked) it would leave a blank
712- // row the user can't fill, so New row opens the add-row form instead, which
713- // inserts the complete row in one request. Full-row inserts (the form, CSV
714- // import, API, blocks, Mothership) need only the insert lock off server-side.
722+ /** A blank grid row is filled in by typing, which is an update, so it needs both locks off. */
715723 const canManualAddRow = userPermissions . canEdit && ! locks ?. insertLocked && ! locks ?. updateLocked
716724 const canEditCellRef = useRef ( canEditCell )
717725 canEditCellRef . current = canEditCell
@@ -1623,6 +1631,11 @@ export function TableGrid({
16231631 const anchorId = contextMenu . row . id
16241632 // Fractional ordering: express intent by neighbor id, not integer position.
16251633 const intent = offset === 0 ? { beforeRowId : anchorId } : { afterRowId : anchorId }
1634+ if ( needsAddRowForm ( updateLockedRef . current , schemaColumnsRef . current ) ) {
1635+ closeContextMenu ( )
1636+ onOpenAddRowModalRef . current ( intent )
1637+ return
1638+ }
16261639 createRef . current (
16271640 { data : { } , ...intent } ,
16281641 {
@@ -1762,7 +1775,10 @@ export function TableGrid({
17621775 // Stable identity so <AddRowButton>'s React.memo still bails out; lock state
17631776 // is read from refs instead of being closed over.
17641777 const handleAddRowClick = useCallback ( ( ) => {
1765- if ( canInsertFullRowRef . current && updateLockedRef . current ) {
1778+ if (
1779+ canInsertFullRowRef . current &&
1780+ needsAddRowForm ( updateLockedRef . current , schemaColumnsRef . current )
1781+ ) {
17661782 onOpenAddRowModalRef . current ( )
17671783 return
17681784 }
@@ -2977,14 +2993,22 @@ export function TableGrid({
29772993
29782994 if ( e . shiftKey && e . key === 'Enter' ) {
29792995 if ( ! canEditRef . current ) return
2980- // Same manual-add path as the Add row button, so it owes the same
2981- // explanation rather than silently doing nothing on a locked table.
2996+ const row = currentRows [ anchor . rowIndex ]
2997+ // Mirrors handleAddRowClick; keep the two new-row paths in sync.
2998+ if (
2999+ row &&
3000+ canInsertFullRowRef . current &&
3001+ needsAddRowForm ( updateLockedRef . current , schemaColumnsRef . current )
3002+ ) {
3003+ e . preventDefault ( )
3004+ onOpenAddRowModalRef . current ( { afterRowId : row . id } )
3005+ return
3006+ }
29823007 if ( ! canManualAddRowRef . current ) {
29833008 e . preventDefault ( )
29843009 onBlockedActionRef . current ( 'add-row' )
29853010 return
29863011 }
2987- const row = currentRows [ anchor . rowIndex ]
29883012 if ( ! row ) return
29893013 e . preventDefault ( )
29903014 const position = row . position + 1
@@ -5119,7 +5143,7 @@ export function TableGrid({
51195143 hasWorkflowColumns = { hasWorkflowColumns }
51205144 workflowCellScoped = { Boolean ( contextMenuGroupId ) }
51215145 disableEdit = { ! canEditCell }
5122- disableInsert = { ! canManualAddRow }
5146+ disableInsert = { ! canInsertFullRow }
51235147 disableDuplicate = { ! canInsertFullRow }
51245148 disableDelete = { ! canDeleteRow }
51255149 onAddToChat = { addToChatRowIds . length > 0 ? handleAddSelectionToChat : undefined }
0 commit comments