@@ -30,7 +30,7 @@ import { formatDate } from '@sim/utils/formatting'
3030import { useQueryState } from 'nuqs'
3131import { saveDiscardActions } from '@/components/settings/save-discard-actions'
3232import type { ShareAuthType } from '@/lib/api/contracts/public-shares'
33- import { isBlockTypeAccessControlExempt } from '@/lib/permission-groups/block-access'
33+ import { isAccessControlAllowlistRow } from '@/lib/permission-groups/block-access'
3434import { PLATFORM_CATEGORY_ORDER , PLATFORM_FEATURES } from '@/lib/permission-groups/features'
3535import type { PermissionGroupConfig } from '@/lib/permission-groups/fields'
3636import { UnsavedChangesModal } from '@/app/workspace/[workspaceId]/components/credential-detail'
@@ -65,6 +65,11 @@ import {
6565 useRemovePermissionGroupMember ,
6666 useUpdatePermissionGroup ,
6767} from '@/ee/access-control/hooks/permission-groups'
68+ import {
69+ allowlistRowsFromStored ,
70+ toggleAllowlistRow ,
71+ withAllowlistRows ,
72+ } from '@/ee/access-control/utils/integration-allowlist-rows'
6873import { SettingRow } from '@/ee/components/setting-row'
6974import { useBlacklistedProviders } from '@/hooks/queries/allowed-providers'
7075import { useOrganizationRoster } from '@/hooks/queries/organization'
@@ -789,9 +794,15 @@ export function GroupDetail({
789794 * otherwise a null→partial transition by a non-revealed admin would silently
790795 * drop a preview block from the stored allowlist and deny it to revealed
791796 * users already running it.
797+ *
798+ * EXCLUDES superseded blocks. They are hidden and so are never rendered, but
799+ * they used to be materialized into the allowlist all the same — so an admin
800+ * narrowing a previously-unrestricted allowlist by unchecking `slack_v2` wrote
801+ * `slack` into it, which the runtime resolves back to `slack_v2` and allows.
802+ * A decision about a retired version is made on its successor's row.
792803 */
793804 const allBlocks = useMemo ( ( ) => {
794- const blocks = getAllBlocks ( ) . filter ( ( b ) => ! isBlockTypeAccessControlExempt ( b . type ) )
805+ const blocks = getAllBlocks ( ) . filter ( ( b ) => isAccessControlAllowlistRow ( b . type ) )
795806 return blocks . sort ( ( a , b ) => {
796807 const catA = BLOCK_CATEGORY_ORDER [ a . category ] ?? 3
797808 const catB = BLOCK_CATEGORY_ORDER [ b . category ] ?? 3
@@ -881,17 +892,16 @@ export function GroupDetail({
881892
882893 const guard = useSettingsUnsavedGuard ( { isDirty : hasChanges } )
883894
895+ const allBlockTypes = useMemo ( ( ) => allBlocks . map ( ( b ) => b . type ) , [ allBlocks ] )
896+
884897 /**
885898 * `null` means "everything allowed". Indexing the allow-lists once keeps the
886899 * per-row membership checks O(1) — they run for every one of the ~200 block
887900 * rows on each render, and again in the section-wide `every(...)` scans.
888901 */
889902 const allowedIntegrationSet = useMemo (
890- ( ) =>
891- editingConfig . allowedIntegrations === null
892- ? null
893- : new Set ( editingConfig . allowedIntegrations ) ,
894- [ editingConfig . allowedIntegrations ]
903+ ( ) => allowlistRowsFromStored ( allBlockTypes , editingConfig . allowedIntegrations ) ,
904+ [ allBlockTypes , editingConfig . allowedIntegrations ]
895905 )
896906
897907 const allowedProviderSet = useMemo (
@@ -994,48 +1004,35 @@ export function GroupDetail({
9941004 const toggleIntegration = useCallback (
9951005 ( blockType : string ) => {
9961006 setEditingConfig ( ( prev ) => {
997- const current = prev . allowedIntegrations
998- let nextAllowed : string [ ] | null
999- if ( current === null ) {
1000- nextAllowed = allBlocks . map ( ( b ) => b . type ) . filter ( ( t ) => t !== blockType )
1001- } else if ( current . includes ( blockType ) ) {
1002- const updated = current . filter ( ( t ) => t !== blockType )
1003- nextAllowed = updated . length === allBlocks . length ? null : updated
1004- } else {
1005- const updated = [ ...current , blockType ]
1006- nextAllowed = updated . length === allBlocks . length ? null : updated
1007- }
1007+ const nextAllowed = toggleAllowlistRow ( allBlockTypes , prev . allowedIntegrations , blockType )
10081008 return {
10091009 ...prev ,
10101010 allowedIntegrations : nextAllowed ,
10111011 deniedTools : pruneDeniedTools ( nextAllowed , prev . deniedTools ) ,
10121012 }
10131013 } )
10141014 } ,
1015- [ allBlocks , pruneDeniedTools ]
1015+ [ allBlockTypes , pruneDeniedTools ]
10161016 )
10171017
10181018 /** Allow or deny a whole section's blocks at once, respecting the active filter. */
10191019 const setBlocksAllowed = useCallback (
10201020 ( blocks : BlockConfig [ ] , allowed : boolean ) => {
10211021 setEditingConfig ( ( prev ) => {
1022- const allTypes = allBlocks . map ( ( b ) => b . type )
1023- const current =
1024- prev . allowedIntegrations === null ? new Set ( allTypes ) : new Set ( prev . allowedIntegrations )
1025- for ( const block of blocks ) {
1026- if ( allowed ) current . add ( block . type )
1027- else current . delete ( block . type )
1028- }
1029- const nextArr = allTypes . filter ( ( t ) => current . has ( t ) )
1030- const nextAllowed = nextArr . length === allTypes . length ? null : nextArr
1022+ const nextAllowed = withAllowlistRows (
1023+ allBlockTypes ,
1024+ prev . allowedIntegrations ,
1025+ blocks . map ( ( block ) => block . type ) ,
1026+ allowed
1027+ )
10311028 return {
10321029 ...prev ,
10331030 allowedIntegrations : nextAllowed ,
10341031 deniedTools : pruneDeniedTools ( nextAllowed , prev . deniedTools ) ,
10351032 }
10361033 } )
10371034 } ,
1038- [ allBlocks , pruneDeniedTools ]
1035+ [ allBlockTypes , pruneDeniedTools ]
10391036 )
10401037
10411038 const isToolAllowed = useCallback (
0 commit comments