@sanity/sdk-react 3.4.0-rc.0 → 3.5.0-next.20260922182803
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/_exports/dashboard.d.ts +151 -45
- package/dist/_exports/dashboard.d.ts.map +1 -1
- package/dist/_exports/dashboard.js +267 -125
- package/dist/_exports/dashboard.js.map +1 -1
- package/dist/index.d.ts +21 -141
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +66 -147
- package/dist/index.js.map +1 -1
- package/dist/{useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js → useStudioWorkspacesByProjectIdDataset-DL6_9jgU.js} +61 -4
- package/dist/useStudioWorkspacesByProjectIdDataset-DL6_9jgU.js.map +1 -0
- package/package.json +9 -9
- package/src/_exports/dashboard.test-d.ts +3 -0
- package/src/_exports/dashboard.ts +13 -2
- package/src/_exports/sdk-react.ts +0 -3
- package/src/components/auth/AuthBoundary.test.tsx +2 -81
- package/src/components/auth/AuthBoundary.tsx +3 -17
- package/src/components/auth/LoginCallback.test.tsx +7 -46
- package/src/components/auth/LoginCallback.tsx +4 -22
- package/src/hooks/dashboard/useAgentResourceContext.test.tsx +67 -2
- package/src/hooks/dashboard/useAgentResourceContext.ts +35 -6
- package/src/hooks/dashboard/useApplicationContext.test.tsx +95 -0
- package/src/hooks/dashboard/useApplicationContext.ts +47 -0
- package/src/hooks/dashboard/useApplications.test.tsx +165 -7
- package/src/hooks/dashboard/useApplications.ts +112 -28
- package/src/hooks/dashboard/useCapabilities.test.tsx +84 -0
- package/src/hooks/dashboard/useCapabilities.ts +27 -0
- package/src/hooks/dashboard/useNavigate.test.ts +392 -5
- package/src/hooks/dashboard/useNavigate.ts +169 -26
- package/src/hooks/dashboard/useNavigateToStudioDocument.test.ts +116 -9
- package/src/hooks/dashboard/useNavigateToStudioDocument.ts +110 -39
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.test.ts +99 -1
- package/src/hooks/dashboard/useRecordDocumentHistoryEvent.ts +73 -2
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.test.tsx +23 -1
- package/src/hooks/dashboard/useStudioWorkspacesByProjectIdDataset.ts +2 -1
- package/dist/useStudioWorkspacesByProjectIdDataset-B5A5kBH9.js.map +0 -1
- package/src/hooks/auth/useHandleOAuthCallback.test.tsx +0 -16
- package/src/hooks/auth/useHandleOAuthCallback.tsx +0 -49
- package/src/hooks/auth/useOAuthAuthorize.test.tsx +0 -16
- package/src/hooks/auth/useOAuthAuthorize.tsx +0 -28
- package/src/hooks/auth/useOAuthTokens.test.tsx +0 -240
- package/src/hooks/auth/useOAuthTokens.tsx +0 -95
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
|
|
2
|
+
import {type MessageBusHost, type PayloadOf} from '@sanity/sdk/dashboard'
|
|
1
3
|
import {renderHook} from '@testing-library/react'
|
|
2
|
-
import {beforeEach, describe, expect, it, vi} from 'vitest'
|
|
4
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
3
5
|
|
|
4
|
-
import {AppProviders} from '../../../test/test-utils'
|
|
6
|
+
import {AppProviders, renderHook as renderHookWithInstance} from '../../../test/test-utils'
|
|
5
7
|
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
6
8
|
import {useAgentResourceContext} from './useAgentResourceContext'
|
|
7
9
|
|
|
@@ -243,3 +245,66 @@ describe('useAgentResourceContext', () => {
|
|
|
243
245
|
})
|
|
244
246
|
})
|
|
245
247
|
})
|
|
248
|
+
|
|
249
|
+
describe('useAgentResourceContext (message bus)', () => {
|
|
250
|
+
const MESSAGE_BUS_KEY = Symbol.for('sanity.os.bus')
|
|
251
|
+
let host: MessageBusHost
|
|
252
|
+
|
|
253
|
+
const collect = (): PayloadOf<'applications.context.update'>[] => {
|
|
254
|
+
const payloads: PayloadOf<'applications.context.update'>[] = []
|
|
255
|
+
host.subscribe('applications.context.update', (message) => payloads.push(message.payload))
|
|
256
|
+
return payloads
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
beforeEach(() => {
|
|
260
|
+
vi.stubGlobal('__SANITY_APP_ID__', 'app')
|
|
261
|
+
host = installMessageBus({appId: 'dashboard'})
|
|
262
|
+
})
|
|
263
|
+
|
|
264
|
+
afterEach(() => {
|
|
265
|
+
resetMessageBus()
|
|
266
|
+
delete (globalThis as {[MESSAGE_BUS_KEY]?: unknown})[MESSAGE_BUS_KEY]
|
|
267
|
+
vi.unstubAllGlobals()
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
it('maps the options to an application context payload', () => {
|
|
271
|
+
const payloads = collect()
|
|
272
|
+
|
|
273
|
+
renderHookWithInstance(() =>
|
|
274
|
+
useAgentResourceContext({projectId: 'proj', dataset: 'ds', documentId: 'doc'}),
|
|
275
|
+
)
|
|
276
|
+
|
|
277
|
+
expect(payloads).toEqual([{resource: {id: 'proj.ds', type: 'dataset'}, document: {id: 'doc'}}])
|
|
278
|
+
expect(useWindowConnection).not.toHaveBeenCalled()
|
|
279
|
+
})
|
|
280
|
+
|
|
281
|
+
it('does not re-emit when re-rendered with equal options', () => {
|
|
282
|
+
const payloads = collect()
|
|
283
|
+
|
|
284
|
+
const {rerender} = renderHookWithInstance((options) => useAgentResourceContext(options), {
|
|
285
|
+
initialProps: {projectId: 'proj', dataset: 'ds', documentId: 'doc'},
|
|
286
|
+
})
|
|
287
|
+
rerender({projectId: 'proj', dataset: 'ds', documentId: 'doc'})
|
|
288
|
+
|
|
289
|
+
expect(payloads).toHaveLength(1)
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('sets document to null when no documentId is given', () => {
|
|
293
|
+
const payloads = collect()
|
|
294
|
+
|
|
295
|
+
renderHookWithInstance(() => useAgentResourceContext({projectId: 'proj', dataset: 'ds'}))
|
|
296
|
+
|
|
297
|
+
expect(payloads).toEqual([{resource: {id: 'proj.ds', type: 'dataset'}, document: null}])
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
it.each([
|
|
301
|
+
{projectId: '', dataset: 'ds'},
|
|
302
|
+
{projectId: 'proj', dataset: ''},
|
|
303
|
+
])('publishes null when a required field is missing (%o)', ({projectId, dataset}) => {
|
|
304
|
+
const payloads = collect()
|
|
305
|
+
|
|
306
|
+
renderHookWithInstance(() => useAgentResourceContext({projectId, dataset, documentId: 'doc'}))
|
|
307
|
+
|
|
308
|
+
expect(payloads).toEqual([null])
|
|
309
|
+
})
|
|
310
|
+
})
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
/* eslint-disable react-compiler/react-compiler -- the transport branch in `useAgentResourceContext` is a deliberate rules-of-hooks exception; the compiler refuses files that disable it */
|
|
1
2
|
import {type Events, SDK_CHANNEL_NAME, SDK_NODE_NAME} from '@sanity/message-protocol'
|
|
3
|
+
import {isDashboardEnvironment} from '@sanity/sdk/_internal'
|
|
2
4
|
import {type FrameMessage} from '@sanity/sdk/comlink'
|
|
3
|
-
import {
|
|
5
|
+
import {type ApplicationContext} from '@sanity/sdk/dashboard'
|
|
6
|
+
import {useCallback, useEffect, useMemo, useRef} from 'react'
|
|
4
7
|
|
|
5
8
|
import {useWindowConnection} from '../comlink/useWindowConnection'
|
|
9
|
+
import {useApplicationContext} from './useApplicationContext'
|
|
6
10
|
|
|
7
11
|
/**
|
|
8
12
|
* @public
|
|
@@ -24,12 +28,14 @@ export interface AgentResourceContextOptions {
|
|
|
24
28
|
|
|
25
29
|
/**
|
|
26
30
|
* @public
|
|
27
|
-
* Hook for
|
|
28
|
-
*
|
|
29
|
-
*
|
|
31
|
+
* Hook for reporting the resource the user is currently interacting with (for example, which
|
|
32
|
+
* document they're editing) so the Agent can understand their context. The hook reports on mount
|
|
33
|
+
* and again whenever the context changes.
|
|
30
34
|
*
|
|
31
|
-
* The
|
|
32
|
-
*
|
|
35
|
+
* The two Dashboard runtimes differ in transport, but the signature is the same:
|
|
36
|
+
* - In an iframe (Comlink), it emits the `dashboard/v1/events/agent/resource/update` event.
|
|
37
|
+
* - In a federated app (message bus), it publishes {@link useApplicationContext}'s
|
|
38
|
+
* `applications.context.update` topic. Reach for `useApplicationContext` directly in new code.
|
|
33
39
|
*
|
|
34
40
|
* @category Agent
|
|
35
41
|
* @param options - The resource context options containing projectId, dataset, and optional documentId
|
|
@@ -53,6 +59,29 @@ export interface AgentResourceContextOptions {
|
|
|
53
59
|
* ```
|
|
54
60
|
*/
|
|
55
61
|
export function useAgentResourceContext(options: AgentResourceContextOptions): void {
|
|
62
|
+
// The branch is stable: the transport is fixed for the page lifetime, so one set of hooks
|
|
63
|
+
// always runs and the other never does.
|
|
64
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime
|
|
65
|
+
if (isDashboardEnvironment()) return useBusAgentResourceContext(options)
|
|
66
|
+
// eslint-disable-next-line react-hooks/rules-of-hooks -- transport is fixed for the page lifetime
|
|
67
|
+
return useComlinkAgentResourceContext(options)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function useBusAgentResourceContext({projectId, dataset, documentId}: AgentResourceContextOptions) {
|
|
71
|
+
const context = useMemo<ApplicationContext | null>(
|
|
72
|
+
() =>
|
|
73
|
+
projectId && dataset
|
|
74
|
+
? {
|
|
75
|
+
resource: {id: `${projectId}.${dataset}`, type: 'dataset'},
|
|
76
|
+
document: documentId ? {id: documentId} : null,
|
|
77
|
+
}
|
|
78
|
+
: null,
|
|
79
|
+
[projectId, dataset, documentId],
|
|
80
|
+
)
|
|
81
|
+
useApplicationContext(context)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function useComlinkAgentResourceContext(options: AgentResourceContextOptions): void {
|
|
56
85
|
const {projectId, dataset, documentId} = options
|
|
57
86
|
const {sendMessage} = useWindowConnection<Events.AgentResourceUpdateMessage, FrameMessage>({
|
|
58
87
|
name: SDK_NODE_NAME,
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
|
|
2
|
+
import {
|
|
3
|
+
type ApplicationContext,
|
|
4
|
+
type MessageBusHost,
|
|
5
|
+
type MessageBusMeta,
|
|
6
|
+
type PayloadOf,
|
|
7
|
+
} from '@sanity/sdk/dashboard'
|
|
8
|
+
import {afterEach, beforeEach, describe, expect, it, vi} from 'vitest'
|
|
9
|
+
|
|
10
|
+
import {renderHook} from '../../../test/test-utils'
|
|
11
|
+
import {useApplicationContext} from './useApplicationContext'
|
|
12
|
+
|
|
13
|
+
type ContextUpdate = {
|
|
14
|
+
payload: PayloadOf<'applications.context.update'>
|
|
15
|
+
meta: MessageBusMeta
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MESSAGE_BUS_KEY = Symbol.for('sanity.os.bus')
|
|
19
|
+
|
|
20
|
+
let host: MessageBusHost
|
|
21
|
+
|
|
22
|
+
const context: ApplicationContext = {
|
|
23
|
+
resource: {id: 'proj.ds', type: 'dataset'},
|
|
24
|
+
document: {id: 'doc'},
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
describe('useApplicationContext', () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
// The SDK resolves its own app ID from the CLI-embedded global.
|
|
30
|
+
vi.stubGlobal('__SANITY_APP_ID__', 'app')
|
|
31
|
+
host = installMessageBus({appId: 'dashboard'})
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
afterEach(() => {
|
|
35
|
+
resetMessageBus()
|
|
36
|
+
delete (globalThis as {[MESSAGE_BUS_KEY]?: unknown})[MESSAGE_BUS_KEY]
|
|
37
|
+
vi.unstubAllGlobals()
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const collect = (): ContextUpdate[] => {
|
|
41
|
+
const messages: ContextUpdate[] = []
|
|
42
|
+
host.subscribe('applications.context.update', (message) =>
|
|
43
|
+
messages.push({payload: message.payload, meta: message.meta}),
|
|
44
|
+
)
|
|
45
|
+
return messages
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
it('emits the context on mount', () => {
|
|
49
|
+
const messages = collect()
|
|
50
|
+
|
|
51
|
+
renderHook(() => useApplicationContext(context))
|
|
52
|
+
|
|
53
|
+
expect(messages.map((message) => message.payload)).toEqual([context])
|
|
54
|
+
})
|
|
55
|
+
|
|
56
|
+
it('does not re-emit when re-rendered with the same context', () => {
|
|
57
|
+
const messages = collect()
|
|
58
|
+
|
|
59
|
+
const {rerender} = renderHook(({value}) => useApplicationContext(value), {
|
|
60
|
+
initialProps: {value: context},
|
|
61
|
+
})
|
|
62
|
+
rerender({value: context})
|
|
63
|
+
|
|
64
|
+
expect(messages).toHaveLength(1)
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
it('clears the old context before publishing a new one', () => {
|
|
68
|
+
const messages = collect()
|
|
69
|
+
|
|
70
|
+
const {rerender} = renderHook(({value}) => useApplicationContext(value), {
|
|
71
|
+
initialProps: {value: context},
|
|
72
|
+
})
|
|
73
|
+
const next: ApplicationContext = {resource: {id: 'proj.ds', type: 'dataset'}, document: null}
|
|
74
|
+
rerender({value: next})
|
|
75
|
+
|
|
76
|
+
expect(messages.map((message) => message.payload)).toEqual([context, null, next])
|
|
77
|
+
})
|
|
78
|
+
|
|
79
|
+
it('emits null on unmount', () => {
|
|
80
|
+
const messages = collect()
|
|
81
|
+
|
|
82
|
+
const {unmount} = renderHook(() => useApplicationContext(context))
|
|
83
|
+
unmount()
|
|
84
|
+
|
|
85
|
+
expect(messages.map((message) => message.payload)).toEqual([context, null])
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('stamps the sending application id on the message', () => {
|
|
89
|
+
const messages = collect()
|
|
90
|
+
|
|
91
|
+
renderHook(() => useApplicationContext(context))
|
|
92
|
+
|
|
93
|
+
expect(messages[0].meta.appId).toBe('app')
|
|
94
|
+
})
|
|
95
|
+
})
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import {type ApplicationContext} from '@sanity/sdk/dashboard'
|
|
2
|
+
import {useEffect} from 'react'
|
|
3
|
+
|
|
4
|
+
import {useEmit} from './useEmit'
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Publishes what this application is currently showing to the Dashboard message bus, so the host
|
|
8
|
+
* can hold the foreground application's context. Pass `null` when nothing is open.
|
|
9
|
+
*
|
|
10
|
+
* The context is published on mount and whenever it changes, and cleared with `null` on unmount.
|
|
11
|
+
* A change publishes `null` before the new context, as the component has stopped showing the old
|
|
12
|
+
* one. Pass a memoised value: the hook compares by reference, so a new object on every render
|
|
13
|
+
* republishes on every render.
|
|
14
|
+
*
|
|
15
|
+
* The host reads the sending application from `message.meta.appId`, so the payload carries no
|
|
16
|
+
* application ID.
|
|
17
|
+
*
|
|
18
|
+
* @param context - The application context to publish, or `null` to clear it
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* ```tsx
|
|
22
|
+
* import {useApplicationContext} from '@sanity/sdk-react/dashboard'
|
|
23
|
+
* import {useMemo} from 'react'
|
|
24
|
+
*
|
|
25
|
+
* function DocumentView({documentId}: {documentId: string}) {
|
|
26
|
+
* const context = useMemo(
|
|
27
|
+
* () => ({resource: {id: 'my-project.production', type: 'dataset' as const}, document: {id: documentId}}),
|
|
28
|
+
* [documentId],
|
|
29
|
+
* )
|
|
30
|
+
* useApplicationContext(context)
|
|
31
|
+
* return <div>Editing {documentId}</div>
|
|
32
|
+
* }
|
|
33
|
+
* ```
|
|
34
|
+
*
|
|
35
|
+
* @public
|
|
36
|
+
* @category Dashboard
|
|
37
|
+
*/
|
|
38
|
+
export function useApplicationContext(context: ApplicationContext | null): void {
|
|
39
|
+
const emit = useEmit('applications.context.update')
|
|
40
|
+
|
|
41
|
+
useEffect(() => {
|
|
42
|
+
emit(context)
|
|
43
|
+
return () => {
|
|
44
|
+
emit(null)
|
|
45
|
+
}
|
|
46
|
+
}, [emit, context])
|
|
47
|
+
}
|
|
@@ -1,5 +1,16 @@
|
|
|
1
|
+
import {
|
|
2
|
+
type Application,
|
|
3
|
+
type ApplicationInclude,
|
|
4
|
+
type Installation,
|
|
5
|
+
type InstallationInclude,
|
|
6
|
+
} from '@sanity/sdk'
|
|
1
7
|
import {installMessageBus, resetMessageBus} from '@sanity/sdk/_internal'
|
|
2
|
-
import {
|
|
8
|
+
import {
|
|
9
|
+
type LocalApplication,
|
|
10
|
+
type MessageBusHost,
|
|
11
|
+
TopicError,
|
|
12
|
+
type ValueOf,
|
|
13
|
+
} from '@sanity/sdk/dashboard'
|
|
3
14
|
import {Suspense} from 'react'
|
|
4
15
|
import {ErrorBoundary} from 'react-error-boundary'
|
|
5
16
|
import {afterEach, beforeEach, describe, expect, expectTypeOf, it, vi} from 'vitest'
|
|
@@ -62,7 +73,7 @@ const application = {
|
|
|
62
73
|
title: 'Summary',
|
|
63
74
|
version: '1',
|
|
64
75
|
moduleId: 'views/summary',
|
|
65
|
-
metadata:
|
|
76
|
+
metadata: {size: 'small'},
|
|
66
77
|
},
|
|
67
78
|
{
|
|
68
79
|
id: 'asset-source-1',
|
|
@@ -84,7 +95,7 @@ const application = {
|
|
|
84
95
|
},
|
|
85
96
|
],
|
|
86
97
|
},
|
|
87
|
-
}
|
|
98
|
+
} satisfies Application<ApplicationInclude>
|
|
88
99
|
|
|
89
100
|
const nonFederatedApplication = {
|
|
90
101
|
...application,
|
|
@@ -95,7 +106,7 @@ const nonFederatedApplication = {
|
|
|
95
106
|
title: 'Legacy',
|
|
96
107
|
isSingleton: false,
|
|
97
108
|
config: {},
|
|
98
|
-
}
|
|
109
|
+
} satisfies Application<ApplicationInclude>
|
|
99
110
|
|
|
100
111
|
const nonSingletonApplication = {
|
|
101
112
|
...application,
|
|
@@ -105,7 +116,7 @@ const nonSingletonApplication = {
|
|
|
105
116
|
slug: 'canvas',
|
|
106
117
|
title: 'Canvas',
|
|
107
118
|
isSingleton: false,
|
|
108
|
-
}
|
|
119
|
+
} satisfies Application<ApplicationInclude>
|
|
109
120
|
|
|
110
121
|
const externalApplication = {
|
|
111
122
|
...application,
|
|
@@ -113,7 +124,57 @@ const externalApplication = {
|
|
|
113
124
|
name: 'external',
|
|
114
125
|
slug: null,
|
|
115
126
|
externalUrl: 'https://apps.example.com/external/index.html',
|
|
116
|
-
}
|
|
127
|
+
} satisfies Application<ApplicationInclude>
|
|
128
|
+
|
|
129
|
+
// The workbench synthesises a dev-server app into the deployed shape and marks it with `local`.
|
|
130
|
+
const localApplication = {
|
|
131
|
+
...application,
|
|
132
|
+
id: 'application-5',
|
|
133
|
+
name: 'dev',
|
|
134
|
+
slug: null,
|
|
135
|
+
isSingleton: false,
|
|
136
|
+
externalUrl: 'http://localhost:3333',
|
|
137
|
+
organizationId: 'local',
|
|
138
|
+
local: {host: 'localhost', port: 3333},
|
|
139
|
+
} satisfies LocalApplication
|
|
140
|
+
|
|
141
|
+
const installation = {
|
|
142
|
+
id: 'installation-1',
|
|
143
|
+
applicationId: 'app-remote-1',
|
|
144
|
+
organizationId: 'organization-1',
|
|
145
|
+
installedBy: 'user-1',
|
|
146
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
147
|
+
updatedAt: '2026-01-02T00:00:00.000Z',
|
|
148
|
+
application: {
|
|
149
|
+
title: 'Remote App',
|
|
150
|
+
name: 'remote',
|
|
151
|
+
reference: 'sanity/remote',
|
|
152
|
+
slug: 'remote',
|
|
153
|
+
icon: null,
|
|
154
|
+
// Distinct from the installing org: the bundle is hosted under the publisher's org.
|
|
155
|
+
organizationId: 'organization-publisher',
|
|
156
|
+
},
|
|
157
|
+
interfaces: [
|
|
158
|
+
{
|
|
159
|
+
id: 'remote-view-1',
|
|
160
|
+
type: 'app',
|
|
161
|
+
name: 'remote',
|
|
162
|
+
title: 'Remote App',
|
|
163
|
+
version: '1',
|
|
164
|
+
moduleId: 'App',
|
|
165
|
+
metadata: null,
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
id: 'remote-panel-1',
|
|
169
|
+
type: 'panel',
|
|
170
|
+
name: 'remote-panel',
|
|
171
|
+
title: 'Remote Panel',
|
|
172
|
+
version: '1',
|
|
173
|
+
moduleId: 'views/panel',
|
|
174
|
+
metadata: null,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
} satisfies Installation<InstallationInclude>
|
|
117
178
|
|
|
118
179
|
const emitApplications = (value: unknown[]) =>
|
|
119
180
|
host.connections.subscribe((client) =>
|
|
@@ -140,8 +201,12 @@ describe('useApplications', () => {
|
|
|
140
201
|
const {result} = renderHook(() => useApplications())
|
|
141
202
|
|
|
142
203
|
expectTypeOf(result.current).toEqualTypeOf<DashboardApplication[]>()
|
|
204
|
+
// The application member keeps its raw deployment fields; the union also admits installations.
|
|
143
205
|
expectTypeOf<
|
|
144
|
-
Extract<
|
|
206
|
+
Extract<
|
|
207
|
+
keyof Exclude<DashboardApplication, {type: 'installation'}>,
|
|
208
|
+
'activeDeployment' | 'config'
|
|
209
|
+
>
|
|
145
210
|
>().toEqualTypeOf<'activeDeployment' | 'config'>()
|
|
146
211
|
const [federated, nonFederated, nonSingleton] = result.current
|
|
147
212
|
expect(federated).toMatchObject({
|
|
@@ -180,6 +245,99 @@ describe('useApplications', () => {
|
|
|
180
245
|
])
|
|
181
246
|
expect(nonFederated).toMatchObject({views: [], webWorkers: []})
|
|
182
247
|
expect(nonSingleton?.views[0]?.module.entry).toBe('https://canvas.sanity.studio')
|
|
248
|
+
expect(result.current.map(({isLocal}) => isLocal)).toEqual([false, false, false])
|
|
249
|
+
})
|
|
250
|
+
|
|
251
|
+
it('marks dev-server applications local and loads their modules from the dev server', () => {
|
|
252
|
+
emitApplications([application, localApplication, installation])
|
|
253
|
+
|
|
254
|
+
const {result} = renderHook(() => useApplications())
|
|
255
|
+
|
|
256
|
+
expect(result.current.map(({id, isLocal}) => [id, isLocal])).toEqual([
|
|
257
|
+
['application-1', false],
|
|
258
|
+
['application-5', true],
|
|
259
|
+
['installation-1', false],
|
|
260
|
+
])
|
|
261
|
+
const local = result.current[1]
|
|
262
|
+
expect(local).toMatchObject({type: 'coreApp', local: {host: 'localhost', port: 3333}})
|
|
263
|
+
expect(local?.views[0]?.module.entry).toBe('http://localhost:3333')
|
|
264
|
+
expect(local?.views[0]?.application).toMatchObject({id: 'application-5', isLocal: true})
|
|
265
|
+
})
|
|
266
|
+
|
|
267
|
+
it('shapes installations alongside applications in one consistent shape', () => {
|
|
268
|
+
emitApplications([application, installation])
|
|
269
|
+
|
|
270
|
+
const {result} = renderHook(() => useApplications())
|
|
271
|
+
|
|
272
|
+
const shaped = result.current.find(({id}) => id === 'installation-1')
|
|
273
|
+
expect(shaped).toMatchObject({
|
|
274
|
+
id: 'installation-1',
|
|
275
|
+
type: 'installation',
|
|
276
|
+
title: 'Remote App',
|
|
277
|
+
name: 'remote',
|
|
278
|
+
reference: 'sanity/remote',
|
|
279
|
+
slug: 'remote',
|
|
280
|
+
icon: null,
|
|
281
|
+
isSingleton: true,
|
|
282
|
+
visibility: 'default',
|
|
283
|
+
externalUrl: null,
|
|
284
|
+
organizationId: 'organization-publisher',
|
|
285
|
+
createdAt: '2026-01-01T00:00:00.000Z',
|
|
286
|
+
updatedAt: '2026-01-02T00:00:00.000Z',
|
|
287
|
+
installation,
|
|
288
|
+
})
|
|
289
|
+
// Raw record fields must not leak onto the shaped entry, nor the raw record onto its views.
|
|
290
|
+
expect(shaped).not.toHaveProperty('applicationId')
|
|
291
|
+
expect(shaped).not.toHaveProperty('installedBy')
|
|
292
|
+
expect(shaped?.views[0]?.application).not.toHaveProperty('installation')
|
|
293
|
+
expect(shaped?.views).toEqual([
|
|
294
|
+
expect.objectContaining({
|
|
295
|
+
name: 'remote',
|
|
296
|
+
surface: 'window',
|
|
297
|
+
module: {
|
|
298
|
+
entry: 'https://remote-apps-organization-publisher.sanity.run',
|
|
299
|
+
moduleId: 'installation-1/App',
|
|
300
|
+
version: '1',
|
|
301
|
+
},
|
|
302
|
+
}),
|
|
303
|
+
expect.objectContaining({
|
|
304
|
+
name: 'remote-panel',
|
|
305
|
+
surface: 'panel',
|
|
306
|
+
module: expect.objectContaining({moduleId: 'installation-1/views/panel'}),
|
|
307
|
+
}),
|
|
308
|
+
])
|
|
309
|
+
expect(shaped?.webWorkers).toEqual([])
|
|
310
|
+
})
|
|
311
|
+
|
|
312
|
+
it('exposes no views for an installation without interfaces', () => {
|
|
313
|
+
const {interfaces: _interfaces, ...withoutInterfaces} = installation
|
|
314
|
+
emitApplications([withoutInterfaces])
|
|
315
|
+
|
|
316
|
+
const {result} = renderHook(() => useApplications())
|
|
317
|
+
|
|
318
|
+
expect(result.current[0]).toMatchObject({id: 'installation-1', views: [], webWorkers: []})
|
|
319
|
+
})
|
|
320
|
+
|
|
321
|
+
it('exposes no views for an installation without a slug to derive an origin from', () => {
|
|
322
|
+
emitApplications([{...installation, application: {...installation.application, slug: null}}])
|
|
323
|
+
|
|
324
|
+
const {result} = renderHook(() => useApplications())
|
|
325
|
+
|
|
326
|
+
expect(result.current[0]).toMatchObject({id: 'installation-1', views: [], webWorkers: []})
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
it('loads a dev-server application without a module federation manifest', () => {
|
|
330
|
+
const {config: _config, ...withoutManifest} = localApplication
|
|
331
|
+
emitApplications([withoutManifest])
|
|
332
|
+
|
|
333
|
+
const {result} = renderHook(() => useApplications())
|
|
334
|
+
|
|
335
|
+
expect(result.current[0]?.views.map(({name}) => name)).toEqual([
|
|
336
|
+
'inbox',
|
|
337
|
+
'notifications',
|
|
338
|
+
'summary',
|
|
339
|
+
'library',
|
|
340
|
+
])
|
|
183
341
|
})
|
|
184
342
|
|
|
185
343
|
it('follows topic updates without remapping unchanged lists', () => {
|