@@ -513,6 +513,24 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved,
513513 } ) ;
514514 }
515515
516+ // objectui#2600 B4 — field-level bulk over the fields currently VISIBLE in the
517+ // sub-table (i.e. after the field filter), mirroring the per-object row's
518+ // read/all/none shortcuts. `readable` sets read-only, `writable` grants R+W,
519+ // `clear` drops the explicit overrides so those fields fall back to default.
520+ function bulkSetFields ( objectName : string , action : 'readable' | 'writable' | 'clear' , fieldNames : string [ ] ) {
521+ if ( fieldNames . length === 0 ) return ;
522+ setDraft ( ( prev ) => {
523+ const fields = { ...( prev . fields ?? { } ) } ;
524+ for ( const fieldName of fieldNames ) {
525+ const key = `${ objectName } .${ fieldName } ` ;
526+ if ( action === 'clear' ) delete fields [ key ] ;
527+ else if ( action === 'readable' ) fields [ key ] = { readable : true , editable : false } ;
528+ else fields [ key ] = { readable : true , editable : true } ;
529+ }
530+ return { ...prev , fields } ;
531+ } ) ;
532+ }
533+
516534 /**
517535 * Re-narrow a freshly-read full permission set to this package's slice for
518536 * display. No-op at environment scope (no `packageId`).
@@ -815,6 +833,7 @@ export function PermissionMatrixEditPage({ type, name, packageId, onDraftSaved,
815833 onObjectPerm = { updateObjectPerm }
816834 onFieldPerm = { updateFieldPerm }
817835 onBulkSet = { bulkSetObject }
836+ onFieldBulk = { bulkSetFields }
818837 onOpenOwd = { onOpenOwd }
819838 />
820839 { /* Advanced facets (ADR-0056 P3) — RLS / tab visibility / delegated
@@ -909,6 +928,8 @@ interface PermissionTableProps {
909928 onObjectPerm : ( objectName : string , action : keyof ObjectPerm , value : boolean ) => void ;
910929 onFieldPerm : ( objectName : string , fieldName : string , action : keyof FieldPerm , value : boolean ) => void ;
911930 onBulkSet : ( objectName : string , action : 'all' | 'none' | 'crud' | 'read' ) => void ;
931+ /** objectui#2600 B4 — field-level bulk over the filtered field set. */
932+ onFieldBulk : ( objectName : string , action : 'readable' | 'writable' | 'clear' , fieldNames : string [ ] ) => void ;
912933 /** objectui#2505 — when set, the OWD badge links to the package OWD overview. */
913934 onOpenOwd ?: ( objectName : string ) => void ;
914935}
@@ -925,6 +946,7 @@ function PermissionTable({
925946 onObjectPerm,
926947 onFieldPerm,
927948 onBulkSet,
949+ onFieldBulk,
928950 onOpenOwd,
929951} : PermissionTableProps ) {
930952 return (
@@ -1078,6 +1100,7 @@ function PermissionTable({
10781100 writable = { writable }
10791101 t = { t }
10801102 onFieldPerm = { onFieldPerm }
1103+ onFieldBulk = { onFieldBulk }
10811104 />
10821105 </ td >
10831106 </ tr >
@@ -1097,14 +1120,19 @@ function FieldsSubTable({
10971120 writable,
10981121 t,
10991122 onFieldPerm,
1123+ onFieldBulk,
11001124} : {
11011125 objectName : string ;
11021126 fields : FieldSummary [ ] | undefined ;
11031127 fieldsState : Record < string , FieldPerm > ;
11041128 writable : boolean ;
11051129 t : ( key : string ) => string ;
11061130 onFieldPerm : ( objectName : string , fieldName : string , action : keyof FieldPerm , value : boolean ) => void ;
1131+ onFieldBulk : ( objectName : string , action : 'readable' | 'writable' | 'clear' , fieldNames : string [ ] ) => void ;
11071132} ) {
1133+ // objectui#2600 B4 — filter fields and bulk-apply over the visible set, so
1134+ // wide objects (dozens of fields) aren't two-checkboxes-at-a-time tedious.
1135+ const [ fieldFilter , setFieldFilter ] = React . useState ( '' ) ;
11081136 if ( ! fields ) {
11091137 return (
11101138 < div className = "text-xs text-muted-foreground flex items-center gap-2" >
@@ -1115,47 +1143,105 @@ function FieldsSubTable({
11151143 if ( fields . length === 0 ) {
11161144 return < div className = "text-xs text-muted-foreground" > { t ( 'perm.field.empty' ) } </ div > ;
11171145 }
1146+ const q = fieldFilter . trim ( ) . toLowerCase ( ) ;
1147+ const visible = q
1148+ ? fields . filter ( ( f ) => f . name . toLowerCase ( ) . includes ( q ) || ( f . label ?? '' ) . toLowerCase ( ) . includes ( q ) )
1149+ : fields ;
1150+ const visibleNames = visible . map ( ( f ) => f . name ) ;
11181151 return (
1119- < table className = "w-full text-xs" >
1120- < thead >
1121- < tr className = "text-muted-foreground" >
1122- < th className = "text-left py-1 font-medium" > { t ( 'perm.field.col.name' ) } </ th >
1123- < th className = "px-2 py-1 font-medium w-16 text-center" title = { t ( 'perm.field.read' ) } > { t ( 'perm.field.read' ) } </ th >
1124- < th className = "px-2 py-1 font-medium w-16 text-center" title = { t ( 'perm.field.edit' ) } > { t ( 'perm.field.edit' ) } </ th >
1125- </ tr >
1126- </ thead >
1127- < tbody >
1128- { fields . map ( ( f ) => {
1129- const key = `${ objectName } .${ f . name } ` ;
1130- const cur = fieldsState [ key ] ?? { readable : true , editable : false } ;
1131- return (
1132- < tr key = { f . name } className = "border-t border-muted" >
1133- < td className = "py-1" >
1134- { f . label ?? f . name }
1135- { f . label && (
1136- < span className = "ml-1 text-muted-foreground" > ({ f . name } )</ span >
1137- ) }
1138- </ td >
1139- < td className = "px-2 py-1 text-center" >
1140- < Checkbox
1141- checked = { ! ! cur . readable }
1142- disabled = { ! writable }
1143- onCheckedChange = { ( v ) => onFieldPerm ( objectName , f . name , 'readable' , ! ! v ) }
1144- aria-label = { `${ objectName } .${ f . name } readable` }
1145- />
1146- </ td >
1147- < td className = "px-2 py-1 text-center" >
1148- < Checkbox
1149- checked = { ! ! cur . editable }
1150- disabled = { ! writable }
1151- onCheckedChange = { ( v ) => onFieldPerm ( objectName , f . name , 'editable' , ! ! v ) }
1152- aria-label = { `${ objectName } .${ f . name } editable` }
1153- />
1154- </ td >
1152+ < div className = "space-y-2" >
1153+ { /* Field toolbar (B4): filter + bulk over the visible field set. The
1154+ filter only appears once one-by-one would actually hurt. */ }
1155+ < div className = "flex flex-wrap items-center gap-2" >
1156+ { fields . length > 6 && (
1157+ < Input
1158+ value = { fieldFilter }
1159+ onChange = { ( e ) => setFieldFilter ( e . target . value ) }
1160+ placeholder = { t ( 'perm.field.filter' ) }
1161+ aria-label = { t ( 'perm.field.filter' ) }
1162+ className = "h-7 w-44 text-xs"
1163+ />
1164+ ) }
1165+ { writable && (
1166+ < div className = "flex items-center gap-1" >
1167+ < Button
1168+ variant = "ghost"
1169+ size = "sm"
1170+ className = "h-6 px-1.5 text-xs"
1171+ disabled = { visibleNames . length === 0 }
1172+ onClick = { ( ) => onFieldBulk ( objectName , 'readable' , visibleNames ) }
1173+ >
1174+ { t ( 'perm.field.bulk.readable' ) }
1175+ </ Button >
1176+ < Button
1177+ variant = "ghost"
1178+ size = "sm"
1179+ className = "h-6 px-1.5 text-xs"
1180+ disabled = { visibleNames . length === 0 }
1181+ onClick = { ( ) => onFieldBulk ( objectName , 'writable' , visibleNames ) }
1182+ >
1183+ { t ( 'perm.field.bulk.writable' ) }
1184+ </ Button >
1185+ < Button
1186+ variant = "ghost"
1187+ size = "sm"
1188+ className = "h-6 px-1.5 text-xs"
1189+ disabled = { visibleNames . length === 0 }
1190+ onClick = { ( ) => onFieldBulk ( objectName , 'clear' , visibleNames ) }
1191+ >
1192+ { t ( 'perm.field.bulk.clear' ) }
1193+ </ Button >
1194+ </ div >
1195+ ) }
1196+ < span className = "ml-auto text-[11px] text-muted-foreground" >
1197+ { visible . length } / { fields . length }
1198+ </ span >
1199+ </ div >
1200+ { visible . length === 0 ? (
1201+ < div className = "text-xs text-muted-foreground" > { t ( 'perm.field.filterEmpty' ) } </ div >
1202+ ) : (
1203+ < table className = "w-full text-xs" >
1204+ < thead >
1205+ < tr className = "text-muted-foreground" >
1206+ < th className = "text-left py-1 font-medium" > { t ( 'perm.field.col.name' ) } </ th >
1207+ < th className = "px-2 py-1 font-medium w-16 text-center" title = { t ( 'perm.field.read' ) } > { t ( 'perm.field.read' ) } </ th >
1208+ < th className = "px-2 py-1 font-medium w-16 text-center" title = { t ( 'perm.field.edit' ) } > { t ( 'perm.field.edit' ) } </ th >
11551209 </ tr >
1156- ) ;
1157- } ) }
1158- </ tbody >
1159- </ table >
1210+ </ thead >
1211+ < tbody >
1212+ { visible . map ( ( f ) => {
1213+ const key = `${ objectName } .${ f . name } ` ;
1214+ const cur = fieldsState [ key ] ?? { readable : true , editable : false } ;
1215+ return (
1216+ < tr key = { f . name } className = "border-t border-muted" >
1217+ < td className = "py-1" >
1218+ { f . label ?? f . name }
1219+ { f . label && (
1220+ < span className = "ml-1 text-muted-foreground" > ({ f . name } )</ span >
1221+ ) }
1222+ </ td >
1223+ < td className = "px-2 py-1 text-center" >
1224+ < Checkbox
1225+ checked = { ! ! cur . readable }
1226+ disabled = { ! writable }
1227+ onCheckedChange = { ( v ) => onFieldPerm ( objectName , f . name , 'readable' , ! ! v ) }
1228+ aria-label = { `${ objectName } .${ f . name } readable` }
1229+ />
1230+ </ td >
1231+ < td className = "px-2 py-1 text-center" >
1232+ < Checkbox
1233+ checked = { ! ! cur . editable }
1234+ disabled = { ! writable }
1235+ onCheckedChange = { ( v ) => onFieldPerm ( objectName , f . name , 'editable' , ! ! v ) }
1236+ aria-label = { `${ objectName } .${ f . name } editable` }
1237+ />
1238+ </ td >
1239+ </ tr >
1240+ ) ;
1241+ } ) }
1242+ </ tbody >
1243+ </ table >
1244+ ) }
1245+ </ div >
11601246 ) ;
11611247}
0 commit comments