@@ -44,7 +44,6 @@ import { useSettingsNavigation } from '@/hooks/use-settings-navigation'
4444import { useLogDetailsUIStore } from '@/stores/logs/store'
4545import type { DeletedRowSnapshot } from '@/stores/table/types'
4646import {
47- type BlockedTableAction ,
4847 type ColumnConfig ,
4948 ColumnConfigSidebar ,
5049 EnrichmentDetails ,
@@ -57,14 +56,18 @@ import {
5756 TableActionBar ,
5857 TableFilter ,
5958 TableGrid ,
60- TableLockedModal ,
6159 type WorkflowConfig ,
6260 WorkflowSidebar ,
6361} from './components'
6462import { COLUMN_SIDEBAR_WIDTH } from './components/table-grid/constants'
6563import { COLUMN_TYPE_ICONS } from './components/table-grid/headers'
6664import { useTable , useTableEventStream } from './hooks'
67- import { describeLocks , lockedNouns } from './lock-copy'
65+ import {
66+ type BlockedTableAction ,
67+ describeBlockedAction ,
68+ describeLocks ,
69+ lockedNouns ,
70+ } from './lock-copy'
6871import {
6972 DEFAULT_TABLE_DETAIL_SORT_DIRECTION ,
7073 tableDetailParsers ,
@@ -83,6 +86,9 @@ const logger = createLogger('Table')
8386 */
8487const tableLocksEnabled = isTruthy ( getEnv ( 'NEXT_PUBLIC_TABLE_LOCKS' ) )
8588
89+ /** Blocked-action toasts carry a button, so they linger past the 5s default. */
90+ const BLOCKED_TOAST_MS = 8000
91+
8692interface TableProps {
8793 /** When set, the table renders without its page header / breadcrumbs / page-level
8894 * options bar. Used by the mothership chat panel to embed a table inline. */
@@ -169,7 +175,9 @@ export function Table({
169175 const [ slideout , dispatch ] = useReducer ( slideoutReducer , { kind : 'none' } )
170176 const [ showDeleteTableConfirm , setShowDeleteTableConfirm ] = useState ( false )
171177 const [ showLockSettings , setShowLockSettings ] = useState ( false )
172- const [ blockedAction , setBlockedAction ] = useState < BlockedTableAction | null > ( null )
178+ // Id of the last blocked-action toast, so a user who keeps typing into a
179+ // locked cell replaces one notice rather than stacking a column of them.
180+ const blockedToastIdRef = useRef < string | null > ( null )
173181 const [ isImportCsvOpen , setIsImportCsvOpen ] = useState ( false )
174182 const [ editingRow , setEditingRow ] = useState < TableRowType | null > ( null )
175183 const [ deletingRows , setDeletingRows ] = useState < DeletedRowSnapshot [ ] > ( [ ] )
@@ -602,6 +610,38 @@ export function Table({
602610 ]
603611 )
604612
613+ // An admin can always reach the settings on a locked table — clearing locks
614+ // stays allowed with the flag off, so the kill switch can't strand one. With
615+ // the flag off and nothing locked there is nothing to change, so the toast is
616+ // a plain notice with no action.
617+ const canOpenLockSettings =
618+ userPermissions . canAdmin === true &&
619+ ( tableLocksEnabled || ( tableData ? lockedNouns ( tableData . locks ) . length > 0 : false ) )
620+
621+ /**
622+ * Explains why a table mutation is unavailable. A toast rather than a modal:
623+ * being told you can't edit shouldn't cost a dismiss click, and admins still
624+ * get a direct route to the settings via the action button.
625+ */
626+ const showBlockedToast = useCallback (
627+ ( action : BlockedTableAction ) => {
628+ if ( ! tableData ) return
629+ if ( blockedToastIdRef . current ) toast . dismiss ( blockedToastIdRef . current )
630+ const { title, text } = describeBlockedAction ( action , tableData . locks )
631+ blockedToastIdRef . current = toast . warning ( title , {
632+ description : text ,
633+ ...( canOpenLockSettings
634+ ? {
635+ action : { label : 'Lock settings' , onClick : ( ) => setShowLockSettings ( true ) } ,
636+ // An action would otherwise pin the toast open until dismissed.
637+ duration : BLOCKED_TOAST_MS ,
638+ }
639+ : { } ) ,
640+ } )
641+ } ,
642+ [ tableData , canOpenLockSettings ]
643+ )
644+
605645 const headerActions = useMemo ( ( ) => {
606646 if ( ! tableData ) return undefined
607647 const anyLocked = lockedNouns ( tableData . locks ) . length > 0
@@ -616,7 +656,7 @@ export function Table({
616656 label : lockLabel ,
617657 icon : Lock ,
618658 onClick : ( ) =>
619- userPermissions . canAdmin ? setShowLockSettings ( true ) : setBlockedAction ( 'status' ) ,
659+ userPermissions . canAdmin ? setShowLockSettings ( true ) : showBlockedToast ( 'status' ) ,
620660 } ,
621661 ]
622662 : [ ] ) ,
@@ -641,6 +681,7 @@ export function Table({
641681 userPermissions . canAdmin ,
642682 handleExportCsv ,
643683 onRequestImportCsv ,
684+ showBlockedToast ,
644685 ] )
645686
646687 // Adding a column is a schema change. The trigger stays visible when the
@@ -651,7 +692,7 @@ export function Table({
651692 trigger = 'header'
652693 disabled = { false }
653694 blocked = { ! canMutateSchema }
654- onBlocked = { ( ) => setBlockedAction ( 'add-column' ) }
695+ onBlocked = { ( ) => showBlockedToast ( 'add-column' ) }
655696 onPickType = { handleAddColumnOfType }
656697 onPickWorkflow = { handleAddWorkflowColumn }
657698 onPickEnrichment = { onOpenEnrichments }
@@ -775,7 +816,7 @@ export function Table({
775816 tableId = { tableId }
776817 embedded = { embedded }
777818 locks = { tableData ?. locks }
778- onBlockedAction = { setBlockedAction }
819+ onBlockedAction = { showBlockedToast }
779820 sidebarReservedPx = { sidebarReservedPx }
780821 onOpenColumnConfig = { onOpenColumnConfig }
781822 onOpenWorkflowConfig = { onOpenWorkflowConfig }
@@ -1039,22 +1080,6 @@ export function Table({
10391080 locks = { tableData . locks }
10401081 />
10411082 ) }
1042- { tableData && (
1043- < TableLockedModal
1044- action = { blockedAction }
1045- locks = { tableData . locks }
1046- // An admin can always reach the panel on a locked table — clearing
1047- // locks stays allowed with the flag off, so the kill switch can't
1048- // strand one. With the flag off and nothing locked there is nothing
1049- // to change, so they get the same read-only notice as everyone else.
1050- canAdmin = {
1051- userPermissions . canAdmin === true &&
1052- ( tableLocksEnabled || lockedNouns ( tableData . locks ) . length > 0 )
1053- }
1054- onClose = { ( ) => setBlockedAction ( null ) }
1055- onOpenLockSettings = { ( ) => setShowLockSettings ( true ) }
1056- />
1057- ) }
10581083 </ Resource >
10591084 )
10601085}
0 commit comments