11'use client'
22
3- import { type ComponentType , useCallback , useMemo , useRef } from 'react'
3+ import { type ComponentType , useCallback , useMemo , useRef , useState } from 'react'
44import {
55 ChevronDown ,
66 ChipInput ,
@@ -11,6 +11,7 @@ import {
1111 DropdownMenuTrigger ,
1212 Search ,
1313} from '@sim/emcn'
14+ import { Lock } from '@sim/emcn/icons'
1415import { useParams } from 'next/navigation'
1516import { useQueryStates } from 'nuqs'
1617import {
@@ -37,7 +38,11 @@ import {
3738 SettingsQueryErrorState ,
3839} from '@/app/workspace/[workspaceId]/settings/components/settings-empty-state'
3940import { SettingsResourceRow } from '@/app/workspace/[workspaceId]/settings/components/settings-resource-row'
40- import { PermissionAccessBoundary } from '@/ee/access-requests/components/permission-access-boundary'
41+ import {
42+ PermissionAccessBoundary ,
43+ useWorkspaceAccessRequestFeatures ,
44+ } from '@/ee/access-requests/components/permission-access-boundary'
45+ import { RequestAccessModal } from '@/ee/access-requests/components/request-access-action'
4146import { useWorkspaceCredentials , type WorkspaceCredential } from '@/hooks/queries/credentials'
4247import { useDebouncedSearchSetter } from '@/hooks/use-debounced-search-setter'
4348import { usePermissionConfig } from '@/hooks/use-permission-config'
@@ -74,6 +79,8 @@ interface IntegrationItemProps {
7479 description ?: string | null
7580 icon : ComponentType < { className ?: string } >
7681 unavailable ?: boolean
82+ /** Set for an integration the viewer's policy restricts but lets them request. */
83+ onRequestAccess ?: ( ) => void
7784}
7885
7986function IntegrationItem ( {
@@ -84,7 +91,9 @@ function IntegrationItem({
8491 description,
8592 icon : Icon ,
8693 unavailable = false ,
94+ onRequestAccess,
8795} : IntegrationItemProps ) {
96+ const restricted = onRequestAccess !== undefined
8897 return (
8998 < SettingsResourceRow
9099 iconVariant = 'custom'
@@ -95,9 +104,15 @@ function IntegrationItem({
95104 ? 'Unavailable in this deployment. Contact your administrator.'
96105 : description || undefined
97106 }
98- href = { `/workspace/${ workspaceId } /integrations/${ slug } ` }
99- clickLabel = { `Open ${ name } ` }
100- navigable = { ! unavailable }
107+ badge = {
108+ restricted ? (
109+ < Lock className = 'size-[14px] text-[var(--text-icon)]' aria-hidden />
110+ ) : undefined
111+ }
112+ href = { restricted ? undefined : `/workspace/${ workspaceId } /integrations/${ slug } ` }
113+ onClick = { onRequestAccess }
114+ clickLabel = { restricted ? `Request access to ${ name } ` : `Open ${ name } ` }
115+ navigable = { ! unavailable && ! restricted }
101116 disabled = { unavailable }
102117 />
103118 )
@@ -156,11 +171,14 @@ function IntegrationsContent() {
156171 const {
157172 integrationAvailability,
158173 isBlockPermitted,
174+ isBlockRequestable,
159175 isIntegrationAvailabilityReady,
160176 isIntegrationAvailabilityFetching,
161177 integrationAvailabilityError,
162178 refetchIntegrationAvailability,
163179 } = usePermissionConfig ( )
180+ const accessRequestsEnabled = useWorkspaceAccessRequestFeatures ( ) . data ?. enabled === true
181+ const [ requestedIntegration , setRequestedIntegration ] = useState < Integration | null > ( null )
164182
165183 const [ { category : urlCategory , search : urlSearchTerm } , setIntegrationFilters ] = useQueryStates (
166184 integrationsParsers ,
@@ -218,29 +236,33 @@ function IntegrationsContent() {
218236 } , [ oauthCredentials ] )
219237
220238 /**
221- * The catalog lists nothing until the deployment allowlist has loaded, so a
222- * pending or failed availability request never shows an integration the
223- * deployment's `ALLOWED_INTEGRATIONS` excludes.
239+ * The catalog lists what the viewer's integration policy permits, plus — when
240+ * access requests are on — what it restricts but lets them request, which
241+ * renders as a locked row. It lists nothing until the deployment allowlist has
242+ * loaded, so a pending or failed availability request never shows an
243+ * integration the deployment's `ALLOWED_INTEGRATIONS` excludes.
224244 */
225- const permittedFeatured = useMemo (
226- ( ) =>
227- isIntegrationAvailabilityReady
228- ? FEATURED_INTEGRATIONS . filter ( ( integration ) => isBlockPermitted ( integration . type ) )
229- : [ ] ,
230- [ isIntegrationAvailabilityReady , isBlockPermitted ]
245+ const isListed = useMemo (
246+ ( ) => ( blockType : string ) =>
247+ isIntegrationAvailabilityReady &&
248+ ( isBlockPermitted ( blockType ) || ( accessRequestsEnabled && isBlockRequestable ( blockType ) ) ) ,
249+ [ isIntegrationAvailabilityReady , isBlockPermitted , accessRequestsEnabled , isBlockRequestable ]
250+ )
251+
252+ const listedFeatured = useMemo (
253+ ( ) => FEATURED_INTEGRATIONS . filter ( ( integration ) => isListed ( integration . type ) ) ,
254+ [ isListed ]
231255 )
232256
233- const permittedCategorySections = useMemo (
257+ const listedCategorySections = useMemo (
234258 ( ) =>
235- isIntegrationAvailabilityReady
236- ? ALL_CATEGORY_SECTIONS . flatMap ( ( section ) => {
237- const integrations = section . integrations . filter ( ( integration ) =>
238- isBlockPermitted ( integration . type )
239- )
240- return integrations . length > 0 ? [ { label : section . label , integrations } ] : [ ]
241- } )
242- : [ ] ,
243- [ isIntegrationAvailabilityReady , isBlockPermitted ]
259+ ALL_CATEGORY_SECTIONS . flatMap ( ( section ) => {
260+ const integrations = section . integrations . filter ( ( integration ) =>
261+ isListed ( integration . type )
262+ )
263+ return integrations . length > 0 ? [ { label : section . label , integrations } ] : [ ]
264+ } ) ,
265+ [ isListed ]
244266 )
245267
246268 const setSelectedCategory = useCallback (
@@ -253,8 +275,8 @@ function IntegrationsContent() {
253275 const categoryOptions = [
254276 ALL_CATEGORY ,
255277 ...( connectedItems . length > 0 ? [ CONNECTED_LABEL ] : [ ] ) ,
256- ...( permittedFeatured . length > 0 ? [ FEATURED_LABEL ] : [ ] ) ,
257- ...permittedCategorySections . map ( ( section ) => section . label ) ,
278+ ...( listedFeatured . length > 0 ? [ FEATURED_LABEL ] : [ ] ) ,
279+ ...listedCategorySections . map ( ( section ) => section . label ) ,
258280 ]
259281
260282 /**
@@ -282,7 +304,7 @@ function IntegrationsContent() {
282304 integration . description . toLowerCase ( ) . includes ( normalizedSearch )
283305
284306 if ( isFeaturedSelected ) {
285- const items = permittedFeatured . filter ( matchesSearch )
307+ const items = listedFeatured . filter ( matchesSearch )
286308 return items . length > 0 ? [ { label : FEATURED_LABEL , integrations : items } ] : [ ]
287309 }
288310
@@ -291,11 +313,11 @@ function IntegrationsContent() {
291313
292314 // Featured is a curated home-row pin: hide it during search so results
293315 // are not duplicated between the Featured section and the category list.
294- const featured = normalizedSearch ? [ ] : permittedFeatured . filter ( matchesCategory )
316+ const featured = normalizedSearch ? [ ] : listedFeatured . filter ( matchesCategory )
295317 const featuredSection =
296318 featured . length > 0 ? [ { label : FEATURED_LABEL , integrations : featured } ] : [ ]
297319
298- const categorySections = permittedCategorySections
320+ const categorySections = listedCategorySections
299321 . filter ( ( section ) => isAllCategorySelected || section . label === selectedCategory )
300322 . map ( ( section ) => ( {
301323 label : section . label ,
@@ -305,8 +327,8 @@ function IntegrationsContent() {
305327
306328 return [ ...featuredSection , ...categorySections ]
307329 } , [
308- permittedFeatured ,
309- permittedCategorySections ,
330+ listedFeatured ,
331+ listedCategorySections ,
310332 isAllCategorySelected ,
311333 isConnectedSelected ,
312334 isFeaturedSelected ,
@@ -349,6 +371,15 @@ function IntegrationsContent() {
349371 return (
350372 < div className = 'flex h-full flex-col bg-[var(--bg)]' >
351373 < IntegrationTabsHeader active = 'integrations' workspaceId = { workspaceId } />
374+ { requestedIntegration && accessRequestsEnabled && (
375+ < RequestAccessModal
376+ key = { requestedIntegration . type }
377+ scope = { { kind : 'workspace' , workspaceId } }
378+ target = { { kind : 'integration' , id : requestedIntegration . type } }
379+ label = { requestedIntegration . name }
380+ onClose = { ( ) => setRequestedIntegration ( null ) }
381+ />
382+ ) }
352383 < div
353384 ref = { scrollContainerRef }
354385 className = 'min-h-0 flex-1 overflow-y-auto px-6 [scrollbar-gutter:stable_both-edges]'
@@ -429,6 +460,11 @@ function IntegrationsContent() {
429460 description = { integration . description }
430461 icon = { Icon }
431462 unavailable = { integration . authType === 'oauth' && deploymentUnavailable }
463+ onRequestAccess = {
464+ isBlockPermitted ( integration . type )
465+ ? undefined
466+ : ( ) => setRequestedIntegration ( integration )
467+ }
432468 />
433469 )
434470 } ) }
0 commit comments