@@ -31,7 +31,6 @@ interface OrganizationSurfaceOrganization {
3131 name : string
3232 slug : string
3333 logo : string | null
34- memberCount : number
3534}
3635
3736interface OrganizationSurfaceViewer {
@@ -46,20 +45,27 @@ interface OrganizationSurfaceViewer {
4645}
4746
4847/**
49- * Everything the organization surface (`/o/[organizationId]`) needs before it renders:
50- * the routed organization's identity, the viewer's standing in it, and the
51- * server-resolved deployment shape its client code reads. A `null`
52- * result is an explicit access denial — the viewer is not a member, or there is no
53- * such organization.
48+ * What deciding a page takes: the routed organization's identity, the viewer's standing in it, and
49+ * the server-resolved deployment shape. A `null` result is an explicit access denial: the viewer is
50+ * not a member, or there is no such organization.
5451 */
55- export interface OrganizationSurfaceContext extends OrganizationChatAccess {
52+ export interface OrganizationSurfaceGate extends OrganizationChatAccess {
5653 organization : OrganizationSurfaceOrganization
5754 viewer : OrganizationSurfaceViewer
5855 connectedAccountsAvailable : boolean
59- settingsFeatures : OrganizationSettingsFeatures
6056 deployment : DeploymentShape
6157}
6258
59+ /**
60+ * Everything the organization surface (`/o/[organizationId]`) hands its client: the
61+ * {@link OrganizationSurfaceGate} plus what only the shell draws, which pages reached by client
62+ * navigation have no use for. Read by the layout, not by each page.
63+ */
64+ export interface OrganizationSurfaceContext extends Omit < OrganizationSurfaceGate , 'organization' > {
65+ organization : OrganizationSurfaceOrganization & { memberCount : number }
66+ settingsFeatures : OrganizationSettingsFeatures
67+ }
68+
6369export function canConnectSearchIntegrations ( config : PermissionGroupConfig | null ) : boolean {
6470 return (
6571 ! capabilityDeniedBy ( 'integrations.manage' , config ) &&
@@ -68,14 +74,14 @@ export function canConnectSearchIntegrations(config: PermissionGroupConfig | nul
6874}
6975
7076/**
71- * Resolves the surface context from membership in the organization named by the
72- * route. Session active-organization state is intentionally not consulted: it
73- * describes the viewer's account, not the organization being viewed.
77+ * Resolves the gate from membership in the organization named by the route. Session
78+ * active-organization state is intentionally not consulted: it describes the viewer's account, not
79+ * the organization being viewed.
7480 */
75- async function resolveOrganizationSurfaceContext (
81+ async function resolveOrganizationSurfaceGate (
7682 organizationId : string ,
7783 userId : string
78- ) : Promise < OrganizationSurfaceContext | null > {
84+ ) : Promise < OrganizationSurfaceGate | null > {
7985 const [ access , [ row ] ] = await Promise . all ( [
8086 getOrganizationSettingsAccess ( organizationId , userId ) ,
8187 db
@@ -91,44 +97,17 @@ async function resolveOrganizationSurfaceContext(
9197 ] )
9298 if ( ! access . isMember || access . role === null || ! row ) return null
9399
94- const deployment = getDeploymentShape ( )
95- const [
96- config ,
97- [ { memberCount } ] ,
98- connectedAccountsAvailable ,
99- searchAccess ,
100- hasEnterprisePlan ,
101- governanceActive ,
102- ] = await Promise . all ( [
100+ const [ config , connectedAccountsAvailable , searchAccess ] = await Promise . all ( [
103101 getUserPermissionConfigForOrganization ( organizationId ) ,
104- db
105- . select ( { memberCount : count ( ) } )
106- . from ( member )
107- . where ( eq ( member . organizationId , organizationId ) ) ,
108102 isScopedCredentialGroupsAvailable ( { kind : 'organization' , organizationId } ) ,
109103 resolveKnowledgeAccessAvailability ( { organizationId } ) ,
110- deployment . hosted && access . isAdmin
111- ? isOrganizationOnEnterprisePlan ( organizationId )
112- : Promise . resolve ( false ) ,
113- /**
114- * Access Control stays listed while a payment is failing, because its rules still apply.
115- *
116- * Resolved rather than rejected on a read failure: this value only decides whether a nav item
117- * is drawn, and it is shared by every organization page — letting it throw would take home,
118- * chat and search down with the billing table. The page and the management API read the same
119- * regime and still fail closed, so a listed item cannot be used to reach anything.
120- */
121- deployment . hosted && access . isAdmin
122- ? isOrganizationPermissionRegimeActive ( organizationId ) . catch ( ( ) => false )
123- : Promise . resolve ( false ) ,
124104 ] )
125105 return {
126106 organization : {
127107 id : row . id ,
128108 name : row . name ,
129109 slug : row . slug ,
130110 logo : row . logo ?? null ,
131- memberCount,
132111 } ,
133112 viewer : {
134113 userId,
@@ -146,19 +125,53 @@ async function resolveOrganizationSurfaceContext(
146125 } ,
147126 connectedAccountsAvailable,
148127 ...projectOrganizationChatAccess ( { role : access . role , config, searchAccess } ) ,
128+ deployment : getDeploymentShape ( ) ,
129+ }
130+ }
131+
132+ /** Request-memoized gate, so a page and the layout above it share one membership lookup. */
133+ export const getOrganizationSurfaceGate = cache ( resolveOrganizationSurfaceGate )
134+
135+ async function resolveOrganizationSurfaceContext (
136+ organizationId : string ,
137+ userId : string
138+ ) : Promise < OrganizationSurfaceContext | null > {
139+ const gate = await getOrganizationSurfaceGate ( organizationId , userId )
140+ if ( ! gate ) return null
141+
142+ const { deployment, viewer } = gate
143+ const [ [ { memberCount } ] , hasEnterprisePlan , governanceActive ] = await Promise . all ( [
144+ db
145+ . select ( { memberCount : count ( ) } )
146+ . from ( member )
147+ . where ( eq ( member . organizationId , organizationId ) ) ,
148+ deployment . hosted && viewer . isAdmin
149+ ? isOrganizationOnEnterprisePlan ( organizationId )
150+ : Promise . resolve ( false ) ,
151+ /**
152+ * Access Control stays listed while a payment is failing, because its rules still apply.
153+ *
154+ * Resolved rather than rejected on a read failure: this value only decides whether a nav item
155+ * is drawn, and it is shared by every organization page — letting it throw would take home,
156+ * chat and search down with the billing table. The page and the management API read the same
157+ * regime and still fail closed, so a listed item cannot be used to reach anything.
158+ */
159+ deployment . hosted && viewer . isAdmin
160+ ? isOrganizationPermissionRegimeActive ( organizationId ) . catch ( ( ) => false )
161+ : Promise . resolve ( false ) ,
162+ ] )
163+ return {
164+ ...gate ,
165+ organization : { ...gate . organization , memberCount } ,
149166 settingsFeatures : getOrganizationSettingsFeatures (
150167 hasEnterprisePlan ,
151168 deployment ,
152169 governanceActive
153170 ) ,
154- deployment,
155171 }
156172}
157173
158- /**
159- * Request-memoized surface context for nested Server Components, so the layout and
160- * any page under it share one membership lookup.
161- */
174+ /** Request-memoized surface context for the layout, sharing its membership lookup with the page's gate. */
162175export const getOrganizationSurfaceContext = cache ( resolveOrganizationSurfaceContext )
163176
164177/**
0 commit comments