@sanity/sdk-react 3.0.0 → 3.2.0
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-internal.d.ts +2 -0
- package/dist/_exports/dashboard-internal.js +2 -0
- package/dist/_exports/dashboard.d.ts +498 -7
- package/dist/_exports/dashboard.d.ts.map +1 -1
- package/dist/_exports/dashboard.js +623 -2
- package/dist/_exports/dashboard.js.map +1 -1
- package/dist/index.d.ts +90 -87
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +10 -115
- package/dist/index.js.map +1 -1
- package/dist/useStudioWorkspacesByProjectIdDataset-BZ_Ud887.js +314 -0
- package/dist/useStudioWorkspacesByProjectIdDataset-BZ_Ud887.js.map +1 -0
- package/package.json +34 -32
- package/src/_exports/dashboard-internal.ts +1 -0
- package/src/_exports/dashboard.test-d.ts +64 -0
- package/src/_exports/dashboard.ts +71 -0
- package/src/_exports/index.ts +1 -1
- package/src/components/auth/AuthBoundary.test.tsx +34 -0
- package/src/components/auth/AuthBoundary.tsx +1 -2
- package/src/context/DashboardTokenRefresh.test.tsx +124 -22
- package/src/context/DashboardTokenRefresh.tsx +70 -20
- package/src/dashboard/createRemoteInstance.test.ts +168 -0
- package/src/dashboard/createRemoteInstance.ts +123 -0
- package/src/dashboard/module.ts +39 -0
- package/src/dashboard/remoteClientState.ts +27 -0
- package/src/dashboard/urlFor.test.ts +246 -0
- package/src/dashboard/urlFor.ts +462 -0
- package/src/hooks/applications/useApplication.test-d.ts +1 -1
- package/src/hooks/dashboard/useApplication.test.tsx +59 -0
- package/src/hooks/dashboard/useApplication.ts +22 -0
- package/src/hooks/dashboard/useApplicationConfig.test.tsx +106 -0
- package/src/hooks/dashboard/useApplicationConfig.ts +45 -0
- package/src/hooks/dashboard/useApplicationConfigs.test.tsx +89 -0
- package/src/hooks/dashboard/useApplicationConfigs.ts +26 -0
- package/src/hooks/dashboard/useApplicationForegroundId.test.tsx +54 -0
- package/src/hooks/dashboard/useApplicationForegroundId.ts +22 -0
- package/src/hooks/dashboard/useApplications.test.tsx +249 -0
- package/src/hooks/dashboard/useApplications.ts +128 -0
- package/src/hooks/dashboard/useEmit.test.tsx +124 -0
- package/src/hooks/dashboard/useEmit.ts +78 -0
- package/src/hooks/dashboard/useNavigate.ts +1 -3
- package/src/hooks/dashboard/useRemoteClient.test.tsx +88 -0
- package/src/hooks/dashboard/useRemoteClient.ts +10 -0
- package/src/hooks/dashboard/useTopic.test.tsx +200 -0
- package/src/hooks/dashboard/useTopic.ts +29 -0
- package/src/hooks/dashboard/useUpdateFavorite.test.tsx +8 -1
- package/src/hooks/document/useApplyDocumentActions.ts +3 -4
- package/src/hooks/document/useCreateDocument.ts +4 -4
- package/src/hooks/document/useDocument.ts +11 -6
- package/src/hooks/document/useEditDocument.ts +5 -5
- package/src/hooks/projection/useDocumentProjection.ts +6 -7
- package/src/hooks/query/useQuery.ts +3 -4
- package/src/utils/resolveOrgResources.test.ts +2 -2
- package/src/utils/resolveOrgResources.ts +2 -2
- package/dist/useStudioWorkspacesByProjectIdDataset-I4S3CuR5.js +0 -177
- package/dist/useStudioWorkspacesByProjectIdDataset-I4S3CuR5.js.map +0 -1
- package/src/context/dashboardToken.ts +0 -63
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import {createInstance as createMFInstance} from '@module-federation/runtime'
|
|
2
|
+
import {
|
|
3
|
+
type ModuleFederationRuntimePlugin,
|
|
4
|
+
type UserOptions,
|
|
5
|
+
} from '@module-federation/runtime/types'
|
|
6
|
+
import {createLogger} from '@sanity/sdk/_internal'
|
|
7
|
+
|
|
8
|
+
type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @public
|
|
12
|
+
*/
|
|
13
|
+
export type FederationRemote = {
|
|
14
|
+
name: string
|
|
15
|
+
entry: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const MANIFEST_FILE = 'mf-manifest.json'
|
|
19
|
+
|
|
20
|
+
function withManifest<T extends {entry: string}>(remote: T): T {
|
|
21
|
+
if (remote.entry.endsWith(`/${MANIFEST_FILE}`)) return remote
|
|
22
|
+
const base = remote.entry.endsWith('/') ? remote.entry : `${remote.entry}/`
|
|
23
|
+
return {...remote, entry: new URL(MANIFEST_FILE, base).href}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @public
|
|
28
|
+
*/
|
|
29
|
+
export type CreateRemoteInstanceOptions = MakeOptional<
|
|
30
|
+
Pick<UserOptions, 'name' | 'remotes' | 'shared' | 'plugins'>,
|
|
31
|
+
'remotes' | 'plugins'
|
|
32
|
+
>
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* @public
|
|
36
|
+
*/
|
|
37
|
+
export interface RemoteInstance {
|
|
38
|
+
registerRemotes(remotes: FederationRemote[]): void
|
|
39
|
+
/** Loads and evaluates a remote expose. */
|
|
40
|
+
loadRemote<T>(id: string): Promise<T | null>
|
|
41
|
+
/** Preloads assets without evaluating them; omit exposes to warm the whole remote. */
|
|
42
|
+
preloadRemote(name: string, exposes?: string[]): Promise<void>
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Creates a client for registering, loading, and preloading federated modules.
|
|
47
|
+
* @public
|
|
48
|
+
*/
|
|
49
|
+
export function createRemoteInstance(options: CreateRemoteInstanceOptions): RemoteInstance {
|
|
50
|
+
const logger = createLogger(options.name)
|
|
51
|
+
const instance = createMFInstance({
|
|
52
|
+
...options,
|
|
53
|
+
plugins: [log(logger.debug), ...(options.plugins ?? [])],
|
|
54
|
+
remotes:
|
|
55
|
+
options.remotes?.map((remote) => ('entry' in remote ? withManifest(remote) : remote)) ?? [],
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
registerRemotes: (remotes) =>
|
|
60
|
+
instance.registerRemotes(remotes.map(withManifest), {force: false}),
|
|
61
|
+
loadRemote: async <T>(id: string) => {
|
|
62
|
+
let remoteModule: T | null
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
remoteModule = await instance.loadRemote<T>(id)
|
|
66
|
+
} catch (error) {
|
|
67
|
+
throw new Error(`Failed to load remote module "${id}"`, {
|
|
68
|
+
cause: error,
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return remoteModule
|
|
73
|
+
},
|
|
74
|
+
// Include async chunks so lazy code is warm before the remote loads.
|
|
75
|
+
preloadRemote: (name, exposes) =>
|
|
76
|
+
instance.preloadRemote([{nameOrAlias: name, exposes, resourceCategory: 'all'}]),
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function log(
|
|
81
|
+
onDebug: (message: string, context?: Record<string, unknown>) => void,
|
|
82
|
+
): ModuleFederationRuntimePlugin {
|
|
83
|
+
const logEvent = (eventName: string, args: object) => {
|
|
84
|
+
// `origin` logs the whole instance, which is noisy
|
|
85
|
+
const {origin: _origin, ...data} = args as Record<string, unknown>
|
|
86
|
+
|
|
87
|
+
onDebug(`[Lifecycle] ${eventName}`, {...data, internal: true})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
name: 'sanity-logger',
|
|
92
|
+
beforeInit(args) {
|
|
93
|
+
logEvent('beforeInit', args)
|
|
94
|
+
return args
|
|
95
|
+
},
|
|
96
|
+
beforeRegisterRemote(args) {
|
|
97
|
+
logEvent('beforeRegisterRemote', args)
|
|
98
|
+
return args
|
|
99
|
+
},
|
|
100
|
+
beforePreloadRemote(args) {
|
|
101
|
+
logEvent('beforePreloadRemote', args)
|
|
102
|
+
},
|
|
103
|
+
beforeRequest(args) {
|
|
104
|
+
logEvent('beforeRequest', args)
|
|
105
|
+
return args
|
|
106
|
+
},
|
|
107
|
+
afterResolve(args) {
|
|
108
|
+
logEvent('afterResolve', args)
|
|
109
|
+
return args
|
|
110
|
+
},
|
|
111
|
+
onLoad(args) {
|
|
112
|
+
logEvent('onLoad', args)
|
|
113
|
+
return args
|
|
114
|
+
},
|
|
115
|
+
loadShare(args) {
|
|
116
|
+
logEvent('loadShare', args)
|
|
117
|
+
},
|
|
118
|
+
beforeLoadShare(args) {
|
|
119
|
+
logEvent('beforeLoadShare', args)
|
|
120
|
+
return args
|
|
121
|
+
},
|
|
122
|
+
}
|
|
123
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import * as React from 'react'
|
|
2
|
+
|
|
3
|
+
const MODULE_SLOT_KEY = Symbol.for('sanity.os.module')
|
|
4
|
+
|
|
5
|
+
type ModuleContext = React.Context<string | undefined>
|
|
6
|
+
type ModuleSlot = WeakMap<typeof React.createContext, ModuleContext>
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Returns the React context that carries the current federation module id.
|
|
10
|
+
*
|
|
11
|
+
* @remarks
|
|
12
|
+
* This is the slot the CLI-generated wrapper populates with
|
|
13
|
+
* `renderOptions.moduleId`. The context lives in a per-React-copy slot on
|
|
14
|
+
* `globalThis` so the CLI wrapper and the SDK share the same context even
|
|
15
|
+
* across module copies. Each React copy gets its own context, since a context
|
|
16
|
+
* created by one copy is inert in another.
|
|
17
|
+
*
|
|
18
|
+
* The slot is keyed on `React.createContext` rather than the `React` namespace
|
|
19
|
+
* object: `import * as React` and `import React from 'react'` can yield
|
|
20
|
+
* different wrapper objects for the same React copy under bundler interop,
|
|
21
|
+
* whereas the `createContext` function is the same reference under both.
|
|
22
|
+
*
|
|
23
|
+
* The provider side lives in the CLI, which must not import the SDK:
|
|
24
|
+
* `packages/@sanity/workbench-cli/src/actions/build/render-remote.ts` in
|
|
25
|
+
* `sanity-io/cli` reconstructs this accessor (same symbol, same key) and
|
|
26
|
+
* wraps `App` in the context's `Provider`. Nothing in this repo exports it.
|
|
27
|
+
* @internal
|
|
28
|
+
*/
|
|
29
|
+
export function getDashboardModuleContext(): ModuleContext {
|
|
30
|
+
const globals = globalThis as {[MODULE_SLOT_KEY]?: ModuleSlot}
|
|
31
|
+
const slot = (globals[MODULE_SLOT_KEY] ??= new WeakMap())
|
|
32
|
+
const key = React.createContext
|
|
33
|
+
let context = slot.get(key)
|
|
34
|
+
if (!context) {
|
|
35
|
+
context = React.createContext<string | undefined>(undefined)
|
|
36
|
+
slot.set(key, context)
|
|
37
|
+
}
|
|
38
|
+
return context
|
|
39
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import {type SanityInstance, type StateSource} from '@sanity/sdk'
|
|
2
|
+
import {of} from 'rxjs'
|
|
3
|
+
|
|
4
|
+
import {createRemoteInstance, type RemoteInstance} from './createRemoteInstance'
|
|
5
|
+
|
|
6
|
+
const remoteClientStates = new WeakMap<SanityInstance, StateSource<RemoteInstance>>()
|
|
7
|
+
|
|
8
|
+
/** Returns the instance's remote client state, creating it on first use. @internal */
|
|
9
|
+
export function getRemoteClientState(instance: SanityInstance): StateSource<RemoteInstance> {
|
|
10
|
+
if (instance.isDisposed()) {
|
|
11
|
+
throw new Error('Cannot create a remote client for a disposed Sanity instance')
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const current = remoteClientStates.get(instance)
|
|
15
|
+
if (current) return current
|
|
16
|
+
|
|
17
|
+
const client = createRemoteInstance({name: `sanity-remote-${instance.instanceId}`})
|
|
18
|
+
const state = {
|
|
19
|
+
getCurrent: () => client,
|
|
20
|
+
observable: of(client),
|
|
21
|
+
subscribe: () => () => {},
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
remoteClientStates.set(instance, state)
|
|
25
|
+
instance.onDispose(() => remoteClientStates.delete(instance))
|
|
26
|
+
return state
|
|
27
|
+
}
|
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
import {describe, expect, expectTypeOf, it} from 'vitest'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
type CanvasUrl,
|
|
5
|
+
type CoreApplicationUrl,
|
|
6
|
+
type DashboardUrl,
|
|
7
|
+
type MediaLibraryUrl,
|
|
8
|
+
type StudioIntentUrl,
|
|
9
|
+
type StudioUrl,
|
|
10
|
+
type StudioWorkspaceUrl,
|
|
11
|
+
UrlBuilder,
|
|
12
|
+
urlFor,
|
|
13
|
+
} from './urlFor'
|
|
14
|
+
|
|
15
|
+
class AcmeDocumentUrlBuilder extends UrlBuilder {
|
|
16
|
+
perspective(perspective: string): this {
|
|
17
|
+
return this.edit((url) => url.searchParams.set('perspective', perspective))
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
panel(panelId: string): this {
|
|
21
|
+
return this.edit((url) => {
|
|
22
|
+
url.hash = `panel/${panelId}`
|
|
23
|
+
})
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class AcmeUrlBuilder extends UrlBuilder {
|
|
28
|
+
static readonly namespace = 'acme'
|
|
29
|
+
|
|
30
|
+
context(contextId: string): this {
|
|
31
|
+
return this.append('contexts', contextId)
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
document(documentId: string): AcmeDocumentUrlBuilder {
|
|
35
|
+
return this.transitionTo(AcmeDocumentUrlBuilder, 'documents', documentId)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class PersonaUrlBuilder extends UrlBuilder {
|
|
40
|
+
static readonly namespace = 'persona'
|
|
41
|
+
|
|
42
|
+
person(personId: string): this {
|
|
43
|
+
return this.append('people', personId)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class CanvasAliasUrlBuilder extends UrlBuilder {
|
|
48
|
+
static readonly namespace = 'canvas'
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('studio', () => {
|
|
52
|
+
it('builds studio URLs', () => {
|
|
53
|
+
expect(urlFor.studios().url()).toBe('/studios/')
|
|
54
|
+
expect(urlFor.studios('studio-1').url()).toBe('/studios/studio-1')
|
|
55
|
+
expect(urlFor.studios('studio-1').workspace('default').url()).toBe('/studios/studio-1/default')
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
it('builds edit intent URLs', () => {
|
|
59
|
+
expect(
|
|
60
|
+
urlFor
|
|
61
|
+
.studios('studio-1')
|
|
62
|
+
.workspace('default')
|
|
63
|
+
.intent('edit', {id: 'drafts.document-1', type: 'article'})
|
|
64
|
+
.url(),
|
|
65
|
+
).toBe('/studios/studio-1/default/intent/edit/id=drafts.document-1;type=article/')
|
|
66
|
+
|
|
67
|
+
expect(urlFor.studios('studio-1').intent('edit', {id: 'document-1', mode: 'focus'}).url()).toBe(
|
|
68
|
+
'/studios/studio-1/intent/edit/id=document-1;mode=focus/',
|
|
69
|
+
)
|
|
70
|
+
})
|
|
71
|
+
|
|
72
|
+
it('builds create and release intent URLs', () => {
|
|
73
|
+
expect(
|
|
74
|
+
urlFor.studios('studio-1').intent('create', {template: 'article', type: 'article'}).url(),
|
|
75
|
+
).toBe('/studios/studio-1/intent/create/template=article;type=article/')
|
|
76
|
+
expect(urlFor.studios('studio-1').intent('release', {id: 'release-1'}).url()).toBe(
|
|
77
|
+
'/studios/studio-1/intent/release/id=release-1/',
|
|
78
|
+
)
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
it('adds perspectives and comments to intent URLs', () => {
|
|
82
|
+
const intent = urlFor
|
|
83
|
+
.studios('studio-1')
|
|
84
|
+
.intent('edit', {id: 'document/1', type: 'press release'})
|
|
85
|
+
|
|
86
|
+
expect(intent.perspective('release/1').url()).toBe(
|
|
87
|
+
'/studios/studio-1/intent/edit/id=document%2F1;type=press%20release/?perspective=release%2F1',
|
|
88
|
+
)
|
|
89
|
+
expect(intent.comment('comment/1').url()).toBe(
|
|
90
|
+
'/studios/studio-1/intent/edit/id=document%2F1;type=press%20release;inspect=sanity%2Fcomments;comment=comment%2F1/',
|
|
91
|
+
)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
it('replaces an earlier comment instead of stacking one', () => {
|
|
95
|
+
expect(
|
|
96
|
+
urlFor
|
|
97
|
+
.studios('studio-1')
|
|
98
|
+
.intent('edit', {id: 'document-1'})
|
|
99
|
+
.comment('comment-1')
|
|
100
|
+
.comment('comment-2')
|
|
101
|
+
.url(),
|
|
102
|
+
).toBe(
|
|
103
|
+
'/studios/studio-1/intent/edit/id=document-1;inspect=sanity%2Fcomments;comment=comment-2/',
|
|
104
|
+
)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('builds studio task URLs', () => {
|
|
108
|
+
expect(urlFor.studios('studio-1').workspace('default').task('task/1').url()).toBe(
|
|
109
|
+
'/studios/studio-1/default?selectedTask=task%2F1',
|
|
110
|
+
)
|
|
111
|
+
expect(urlFor.studios('studio-1').intent('edit', {id: 'document-1'}).task('task-1').url()).toBe(
|
|
112
|
+
'/studios/studio-1/intent/edit/id=document-1/?selectedTask=task-1',
|
|
113
|
+
)
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('builds arbitrary studio paths', () => {
|
|
117
|
+
expect(urlFor.studios('studio-1').path('custom', 'document/1').url()).toBe(
|
|
118
|
+
'/studios/studio-1/custom/document/1',
|
|
119
|
+
)
|
|
120
|
+
})
|
|
121
|
+
|
|
122
|
+
it('encodes studio and workspace identifiers', () => {
|
|
123
|
+
expect(urlFor.studios('studio/1').workspace('main space').url()).toBe(
|
|
124
|
+
'/studios/studio%2F1/main%20space',
|
|
125
|
+
)
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('exposes studio-specific builder interfaces', () => {
|
|
129
|
+
expectTypeOf(urlFor.studios()).toEqualTypeOf<DashboardUrl>()
|
|
130
|
+
expectTypeOf(urlFor.studios('studio-1')).toEqualTypeOf<StudioUrl>()
|
|
131
|
+
expectTypeOf(
|
|
132
|
+
urlFor.studios('studio-1').workspace('default'),
|
|
133
|
+
).toEqualTypeOf<StudioWorkspaceUrl>()
|
|
134
|
+
expectTypeOf(
|
|
135
|
+
urlFor.studios('studio-1').intent('edit', {id: 'document-1'}),
|
|
136
|
+
).toEqualTypeOf<StudioIntentUrl>()
|
|
137
|
+
})
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
describe('coreApp', () => {
|
|
141
|
+
it('builds collection and application URLs', () => {
|
|
142
|
+
expect(urlFor.applications().url()).toBe('/applications/')
|
|
143
|
+
expect(urlFor.applications('app-1').url()).toBe('/applications/app-1')
|
|
144
|
+
expect(urlFor.applications('app-1').path('documents', 'document/1').url()).toBe(
|
|
145
|
+
'/applications/app-1/documents/document/1',
|
|
146
|
+
)
|
|
147
|
+
})
|
|
148
|
+
|
|
149
|
+
it('exposes application-specific builder interfaces', () => {
|
|
150
|
+
expectTypeOf(urlFor.applications()).toEqualTypeOf<DashboardUrl>()
|
|
151
|
+
expectTypeOf(urlFor.applications('app-1')).toEqualTypeOf<CoreApplicationUrl>()
|
|
152
|
+
})
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
describe('Media Library', () => {
|
|
156
|
+
it('builds media library URLs', () => {
|
|
157
|
+
expect(urlFor.mediaLibrary().url()).toBe('/media')
|
|
158
|
+
expect(urlFor.mediaLibrary().asset('asset/1').url()).toBe('/media/assets/asset%2F1')
|
|
159
|
+
expect(urlFor.mediaLibrary().collection('collection/1').url()).toBe(
|
|
160
|
+
'/media/collections/collection%2F1',
|
|
161
|
+
)
|
|
162
|
+
})
|
|
163
|
+
|
|
164
|
+
it('exposes the Media Library builder interface', () => {
|
|
165
|
+
expectTypeOf(urlFor.mediaLibrary()).toEqualTypeOf<MediaLibraryUrl>()
|
|
166
|
+
})
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
describe('Canvas', () => {
|
|
170
|
+
it('builds Canvas URLs', () => {
|
|
171
|
+
expect(urlFor.canvas().document('document/1').url()).toBe('/canvas/doc/document%2F1')
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
it('exposes the Canvas builder interface', () => {
|
|
175
|
+
expectTypeOf(urlFor.canvas()).toEqualTypeOf<CanvasUrl>()
|
|
176
|
+
})
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
describe('UrlBuilder', () => {
|
|
180
|
+
it('returns relative and absolute URL forms', () => {
|
|
181
|
+
const origin = 'https://dashboard.sanity.io'
|
|
182
|
+
const builder = new UrlBuilder(new URL('/applications/app-1', origin))
|
|
183
|
+
const absolute = new URL('/applications/app-1', origin)
|
|
184
|
+
|
|
185
|
+
expect(builder.toString()).toBe('/applications/app-1')
|
|
186
|
+
expect(builder.url()).toBe('/applications/app-1')
|
|
187
|
+
expect(builder.toURL({origin})).toEqual(absolute)
|
|
188
|
+
expect(builder.url({origin})).toBe(absolute.href)
|
|
189
|
+
})
|
|
190
|
+
|
|
191
|
+
it('keeps builders immutable', () => {
|
|
192
|
+
const builder = new AcmeUrlBuilder(new URL('https://dashboard.sanity.io/acme'))
|
|
193
|
+
|
|
194
|
+
builder.context('context-1')
|
|
195
|
+
|
|
196
|
+
expect(builder.url()).toBe('/acme')
|
|
197
|
+
})
|
|
198
|
+
|
|
199
|
+
it('extends the URL builder with custom builder classes', () => {
|
|
200
|
+
const extendedUrlFor = urlFor.extend({acme: AcmeUrlBuilder})
|
|
201
|
+
const reextendedUrlFor = extendedUrlFor.extend({persona: PersonaUrlBuilder})
|
|
202
|
+
|
|
203
|
+
expect(
|
|
204
|
+
extendedUrlFor
|
|
205
|
+
.acme()
|
|
206
|
+
.context('context/1')
|
|
207
|
+
.document('document/1')
|
|
208
|
+
.perspective('published')
|
|
209
|
+
.panel('review 1')
|
|
210
|
+
.url(),
|
|
211
|
+
).toBe(
|
|
212
|
+
'/acme/contexts/context%2F1/documents/document%2F1?perspective=published#panel/review%201',
|
|
213
|
+
)
|
|
214
|
+
expect(reextendedUrlFor.persona().person('person/1').url()).toBe('/persona/people/person%2F1')
|
|
215
|
+
expect(reextendedUrlFor.acme().url()).toBe('/acme')
|
|
216
|
+
expect(urlFor).not.toHaveProperty('acme')
|
|
217
|
+
|
|
218
|
+
expectTypeOf(extendedUrlFor.acme()).toEqualTypeOf<AcmeUrlBuilder>()
|
|
219
|
+
expectTypeOf(
|
|
220
|
+
extendedUrlFor.acme().document('document-1'),
|
|
221
|
+
).toEqualTypeOf<AcmeDocumentUrlBuilder>()
|
|
222
|
+
expectTypeOf(reextendedUrlFor.persona()).toEqualTypeOf<PersonaUrlBuilder>()
|
|
223
|
+
})
|
|
224
|
+
|
|
225
|
+
it('rejects duplicate URL namespaces', () => {
|
|
226
|
+
const extendedUrlFor = urlFor.extend({acme: AcmeUrlBuilder})
|
|
227
|
+
|
|
228
|
+
expect(() => urlFor.extend({canvas: AcmeUrlBuilder} as never)).toThrow(
|
|
229
|
+
'URL builder "canvas" already exists',
|
|
230
|
+
)
|
|
231
|
+
expect(() => urlFor.extend({canvasAlias: CanvasAliasUrlBuilder})).toThrow(
|
|
232
|
+
'URL namespace "canvas" already exists',
|
|
233
|
+
)
|
|
234
|
+
expect(() => extendedUrlFor.extend({acmeAlias: AcmeUrlBuilder})).toThrow(
|
|
235
|
+
'URL namespace "acme" already exists',
|
|
236
|
+
)
|
|
237
|
+
expect(() => urlFor.extend({first: AcmeUrlBuilder, second: AcmeUrlBuilder})).toThrow(
|
|
238
|
+
'URL namespace "acme" already exists',
|
|
239
|
+
)
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('builds the home URL', () => {
|
|
243
|
+
expect(urlFor.home().url()).toBe('/')
|
|
244
|
+
expectTypeOf(urlFor.home()).toEqualTypeOf<DashboardUrl>()
|
|
245
|
+
})
|
|
246
|
+
})
|