@@ -82,8 +82,15 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD
8282 const update = useUpdateCustomBlock ( workspaceId )
8383 const remove = useDeleteCustomBlock ( workspaceId )
8484
85- // Source picker (create only). Editing a block never re-points its source.
86- const { data : workspaces = [ ] } = useWorkspacesQuery ( isCreate )
85+ // Needed in both modes: the source picker (create) and the manage gate (edit).
86+ const { data : workspaces = [ ] } = useWorkspacesQuery ( )
87+
88+ // A block is manageable only by an admin of its SOURCE workspace — the same authz
89+ // the publish/update/delete routes enforce. Everyone else gets a read-only view
90+ // (no Save/Discard, no Delete, disabled fields). Create is always manageable: the
91+ // list gates the entry on workspace-admin and the picker only offers admin workspaces.
92+ const canManageBlock =
93+ isCreate || workspaces . some ( ( w ) => w . id === existing ?. workspaceId && w . permissions === 'admin' )
8794 // Custom blocks are org-scoped and the settings list only shows the current org's
8895 // blocks, so a block published to another org's workspace would silently never
8996 // appear here. Restrict the picker to workspaces in the current workspace's org.
@@ -387,14 +394,16 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD
387394 title = { existing ?. name || 'New block' }
388395 description = 'Publish a deployed workflow as a reusable, org-wide block.'
389396 actions = { [
390- ...saveDiscardActions ( {
391- dirty,
392- saving,
393- onSave : handleSave ,
394- onDiscard : handleDiscard ,
395- saveDisabled,
396- } ) ,
397- ...( existing
397+ ...( canManageBlock
398+ ? saveDiscardActions ( {
399+ dirty,
400+ saving,
401+ onSave : handleSave ,
402+ onDiscard : handleDiscard ,
403+ saveDisabled,
404+ } )
405+ : [ ] ) ,
406+ ...( existing && canManageBlock
398407 ? [
399408 {
400409 text : remove . isPending ? 'Deleting...' : 'Delete' ,
@@ -467,11 +476,11 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD
467476
468477 < SettingRow label = 'Icon' labelTooltip = 'Square image (PNG, JPEG, or SVG). Optional.' >
469478 < div className = 'flex items-center gap-4' >
470- < DropZone onDrop = { iconUpload . handleFileDrop } >
479+ < DropZone onDrop = { canManageBlock ? iconUpload . handleFileDrop : ( ) => { } } >
471480 < button
472481 type = 'button'
473482 onClick = { iconUpload . handleThumbnailClick }
474- disabled = { iconUpload . isUploading }
483+ disabled = { iconUpload . isUploading || ! canManageBlock }
475484 className = 'group relative flex size-16 shrink-0 items-center justify-center overflow-hidden rounded-xl border border-[var(--border)] bg-[var(--surface-2)] transition-colors hover:bg-[var(--surface-3)] disabled:opacity-50'
476485 >
477486 { iconUpload . isUploading ? (
@@ -489,7 +498,7 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD
489498 variant = 'outline'
490499 size = 'sm'
491500 onClick = { iconUpload . handleThumbnailClick }
492- disabled = { iconUpload . isUploading }
501+ disabled = { iconUpload . isUploading || ! canManageBlock }
493502 className = 'text-[13px]'
494503 >
495504 { iconUrl ? 'Change' : 'Upload' }
@@ -500,7 +509,7 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD
500509 variant = 'ghost'
501510 size = 'sm'
502511 onClick = { iconUpload . handleRemove }
503- disabled = { iconUpload . isUploading }
512+ disabled = { iconUpload . isUploading || ! canManageBlock }
504513 className = 'text-[13px] text-[var(--text-muted)] hover:text-[var(--text-primary)]'
505514 >
506515 < X className = 'size-[14px]' />
@@ -523,6 +532,7 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD
523532 onChange = { ( e ) => setName ( e . target . value ) }
524533 placeholder = 'Invoice Parser'
525534 maxLength = { 60 }
535+ disabled = { ! canManageBlock }
526536 />
527537 </ SettingRow >
528538
@@ -533,6 +543,7 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD
533543 placeholder = 'What this block does'
534544 rows = { 2 }
535545 maxLength = { 280 }
546+ disabled = { ! canManageBlock }
536547 />
537548 </ SettingRow >
538549
@@ -600,6 +611,7 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD
600611 onChange = { ( e ) => setPlaceholder ( i . id , e . target . value ) }
601612 placeholder = 'Shown in the empty field'
602613 maxLength = { 200 }
614+ disabled = { ! canManageBlock }
603615 />
604616 </ div >
605617 </ div >
@@ -623,7 +635,7 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD
623635 className = 'w-full'
624636 dropdownWidth = 'trigger'
625637 maxHeight = { 280 }
626- disabled = { deployed . isLoading || outputGroups . length === 0 }
638+ disabled = { deployed . isLoading || outputGroups . length === 0 || ! canManageBlock }
627639 emptyMessage = { deployed . isLoading ? 'Loading workflow…' : 'No outputs found.' }
628640 options = { [ ] }
629641 groups = { outputGroups }
@@ -655,6 +667,7 @@ export function CustomBlockDetail({ blockId, workspaceId, onBack }: CustomBlockD
655667 placeholder = 'name'
656668 className = 'w-[140px]'
657669 maxLength = { 60 }
670+ disabled = { ! canManageBlock }
658671 />
659672 </ div >
660673 )
0 commit comments