@sanity/workflow-studio 0.8.0 → 0.9.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/index.js CHANGED
@@ -1,180 +1,112 @@
1
- import { instanceGuardQuery, contentDraftFallback, contentReleaseName, WORKFLOW_INSTANCE_TYPE, ENGINE_API_VERSION, createEngine, EXECUTION_KINDS } from "@sanity/workflow-engine";
1
+ import { ensureDocumentExists, datasetResourceId, useKeyed } from "@sanity/workflow-react/observer";
2
2
 
3
- import { sameDataset, ensureDocumentExists, validatedInstancesStore, validatedGuardsStore, combineGuardStores, combineDocStores, classifyRef, validatedInstanceStore, datasetResourceId, useKeyed } from "@sanity/workflow-react/observer";
4
-
5
- import { sdkInstancesStore, sdkGuardStore, sdkContentDocStore, sdkInstanceDocStore } from "@sanity/workflow-sdk/observer";
3
+ import { makeSdkObserver } from "@sanity/workflow-sdk/observer";
6
4
 
7
5
  import { createSanityInstance } from "@sanity/sdk";
8
6
 
9
- import { useMemo, useRef, useEffect } from "react";
10
-
11
- import { useDocumentStore, useSource, useClient } from "sanity";
7
+ import { ENGINE_API_VERSION, createEngine, EXECUTION_KINDS } from "@sanity/workflow-engine";
12
8
 
13
- import { useWorkflowTelemetry, useWorkflowSession as useWorkflowSession$1, useDocumentWorkflows as useDocumentWorkflows$1, useWorkflowInstances as useWorkflowInstances$1 } from "@sanity/workflow-react";
9
+ import { useMemo, useEffect, useState } from "react";
14
10
 
15
- const RESUBSCRIBE_DELAY_MS = 5e3;
11
+ import { useSource, useClient } from "sanity";
16
12
 
17
- function observableStore({observable: observable, resolve: resolve, initial: initial, name: name}) {
18
- let latest = initial;
19
- return {
20
- subscribe(onChange) {
21
- let subscription, retry, stopped = !1;
22
- const connect = () => {
23
- subscription = observable.subscribe({
24
- next: value => {
25
- latest = resolve(value), onChange();
26
- },
27
- error: err => {
28
- console.error(`[workflow-studio] ${name} stream errored — resubscribing in ${RESUBSCRIBE_DELAY_MS / 1e3}s:`, err),
29
- stopped || (retry = setTimeout(connect, RESUBSCRIBE_DELAY_MS));
30
- }
31
- });
32
- };
33
- return connect(), () => {
34
- stopped = !0, retry !== void 0 && clearTimeout(retry), subscription?.unsubscribe();
35
- };
36
- },
37
- getSnapshot: () => latest
38
- };
39
- }
13
+ import { useWorkflowTelemetry, useWorkflowSession as useWorkflowSession$1, useDocumentWorkflows as useDocumentWorkflows$1, useWorkflowInstances as useWorkflowInstances$1 } from "@sanity/workflow-react";
40
14
 
41
- function makeStudioObserver(documentStore, options) {
42
- const {mounted: mounted, engineResource: engineResource, sdk: sdk, client: client} = options, engineIsMounted = sameDataset(engineResource, mounted), editStateStore = ({id: id, type: type, release: release, draftFallback: draftFallback}) => observableStore({
43
- observable: documentStore.pair.editState(id, type, release),
44
- resolve: editState => resolveEditState(editState, draftFallback),
45
- initial: void 0,
46
- name: `the edit state of "${id}"`
47
- }), requireSdk = what => {
48
- if (sdk !== void 0) return sdk;
49
- throw new Error(`@sanity/workflow-studio: ${what} — Studio's document store is bound to ${mounted.projectId}.${mounted.dataset} and cannot observe it. Pass an App SDK instance via the hook's \`sdk\` option (e.g. \`useWorkflowSession({engine, instanceId, sdk})\`) to observe foreign resources.`);
50
- };
15
+ function makeStudioObserver(sdk, options) {
16
+ const {client: client, engineResource: engineResource} = options;
51
17
  return {
52
- observeInstance: instanceId => engineIsMounted ? validatedInstanceStore(editStateStore({
53
- id: instanceId,
54
- type: WORKFLOW_INSTANCE_TYPE,
55
- release: void 0,
56
- draftFallback: !0
57
- })) : validatedInstanceStore(sdkInstanceDocStore({
58
- sdk: requireSdk(`instance "${instanceId}" lives in the engine dataset ${engineResource.projectId}.${engineResource.dataset}`),
59
- instanceId: instanceId,
18
+ ...makeSdkObserver(sdk, {
60
19
  engineResource: engineResource
61
- })),
62
- observeDocs: (documents, perspective) => combineDocStores(documents.map(ref => classifyRef(ref, mounted) === "mounted-dataset" ? {
63
- key: ref.globalDocumentId,
64
- store: editStateStore({
65
- id: ref.documentId,
66
- type: ref.type,
67
- release: contentReleaseName({
68
- ref: ref,
69
- perspective: perspective
70
- }),
71
- draftFallback: contentDraftFallback({
72
- ref: ref,
73
- perspective: perspective
74
- })
75
- })
76
- } : {
77
- key: ref.globalDocumentId,
78
- store: sdkContentDocStore({
79
- sdk: requireSdk(`"${ref.globalDocumentId}" lives outside this workspace`),
80
- ref: ref,
81
- perspective: perspective
82
- })
83
- })),
84
- observeGuards: (instanceId, resources) => validatedGuardsStore(combineGuardStores(resources.map(resource => {
85
- if (sameDataset(resource, mounted)) {
86
- const {query: query, params: params} = instanceGuardQuery(instanceId);
87
- return observableStore({
88
- observable: documentStore.listenQuery(query, params, {}),
89
- resolve: docs => docs,
90
- initial: void 0,
91
- name: `the guard list for "${instanceId}"`
92
- });
93
- }
94
- return sdkGuardStore({
95
- sdk: requireSdk(`guards for "${instanceId}" live in ${resource.projectId}.${resource.dataset}`),
96
- instanceId: instanceId,
97
- resource: resource
98
- });
99
- }))),
100
- observeInstances: query => engineIsMounted ? validatedInstancesStore(observableStore({
101
- observable: documentStore.listenQuery(query.query, query.params, {}),
102
- resolve: docs => docs,
103
- initial: void 0,
104
- name: "the instance list"
105
- })) : validatedInstancesStore(sdkInstancesStore({
106
- sdk: requireSdk(`the instance list lives in the engine dataset ${engineResource.projectId}.${engineResource.dataset}`),
107
- query: query,
108
- resource: engineResource
109
- })),
20
+ }),
110
21
  ensureDocumentExists: args => ensureDocumentExists(client, args)
111
22
  };
112
23
  }
113
24
 
114
- function resolveEditState(editState, draftFallback) {
115
- if (!editState.ready) return;
116
- const draft = draftFallback ? editState.draft : null;
117
- return editState.version ?? draft ?? editState.published;
118
- }
119
-
120
25
  const SESSION_CLIENT_OPTIONS = {
121
26
  apiVersion: ENGINE_API_VERSION
122
27
  };
123
28
 
124
29
  function useStudioObserver({engine: engine, sdk: suppliedSdk}) {
125
- const documentStore = useDocumentStore(), source = useSource(), {projectId: projectId, dataset: dataset} = source, mounted = useMemo(() => ({
30
+ const source = useSource(), {projectId: projectId, dataset: dataset} = source, mounted = useMemo(() => ({
126
31
  projectId: projectId,
127
32
  dataset: dataset
128
- }), [ projectId, dataset ]), engineResource = useMemo(() => datasetResourceId(engine.workflowResource), [ engine ]), sdk = useEmbeddedSdk({
33
+ }), [ projectId, dataset ]), engineResource = useMemo(() => datasetResourceId(engine.workflowResource), [ engine ]), studio = useStudioAuth(), sdk = useEmbeddedSdk({
129
34
  supplied: suppliedSdk,
130
- needed: !sameDataset(engineResource, mounted),
35
+ owner: source,
131
36
  mounted: mounted,
132
- sessionToken: source.getClient(SESSION_CLIENT_OPTIONS).config().token
37
+ studio: studio
133
38
  });
134
- return useMemo(() => makeStudioObserver(documentStore, {
135
- mounted: mounted,
39
+ return useMemo(() => makeStudioObserver(sdk, {
136
40
  client: source.getClient(SESSION_CLIENT_OPTIONS),
137
- engineResource: engineResource,
138
- ...sdk !== void 0 ? {
139
- sdk: sdk
140
- } : {}
141
- }), [ documentStore, source, mounted, engineResource, sdk ]);
41
+ engineResource: engineResource
42
+ }), [ source, engineResource, sdk ]);
142
43
  }
143
44
 
144
- function useEmbeddedSdk({supplied: supplied, needed: needed, mounted: mounted, sessionToken: sessionToken}) {
145
- const wanted = supplied === void 0 && needed, held = useRef(void 0), key = `${mounted.projectId}.${mounted.dataset}`;
146
- wanted && (held.current?.key !== key || held.current.instance.isDisposed()) && (held.current = {
45
+ function useStudioAuth() {
46
+ const {authenticated: authenticated, auth: auth} = useSource();
47
+ return useMemo(() => {
48
+ const token = auth.token;
49
+ return token === void 0 ? {
50
+ authenticated: authenticated
51
+ } : {
52
+ authenticated: authenticated,
53
+ auth: {
54
+ token: token
55
+ }
56
+ };
57
+ }, [ authenticated, auth.token ]);
58
+ }
59
+
60
+ function useEmbeddedSdk({supplied: supplied, owner: owner, mounted: mounted, studio: studio}) {
61
+ const resolved = supplied === void 0 ? {
62
+ kind: "shared",
63
+ shared: sharedEmbeddedSdk({
64
+ owner: owner,
65
+ mounted: mounted,
66
+ studio: studio
67
+ })
68
+ } : {
69
+ kind: "supplied",
70
+ instance: supplied
71
+ };
72
+ return useSharedDispose(owner, resolved.kind === "shared" ? resolved.shared : void 0),
73
+ resolved.kind === "shared" ? resolved.shared.instance : resolved.instance;
74
+ }
75
+
76
+ const sharedSdks = /* @__PURE__ */ new WeakMap;
77
+
78
+ function sharedEmbeddedSdk({owner: owner, mounted: mounted, studio: studio}) {
79
+ const key = `${mounted.projectId}.${mounted.dataset}`;
80
+ let byResource = sharedSdks.get(owner);
81
+ byResource === void 0 && (byResource = /* @__PURE__ */ new Map, sharedSdks.set(owner, byResource));
82
+ const current = byResource.get(key);
83
+ if (current !== void 0 && !current.instance.isDisposed()) return current;
84
+ const shared = {
85
+ instance: bootstrapInstance(mounted, studio),
147
86
  key: key,
148
- instance: bootstrapInstance(mounted, sessionToken)
149
- });
150
- const embedded = wanted ? held.current?.instance : void 0;
151
- return useDeferredDispose(embedded), supplied ?? embedded;
87
+ users: 0
88
+ };
89
+ return byResource.set(key, shared), shared;
152
90
  }
153
91
 
154
- function bootstrapInstance(mounted, sessionToken) {
155
- if (sessionToken === void 0) throw new Error("@sanity/workflow-studio: the engine's state dataset is not this workspace's dataset, and no Studio session token is readable to bootstrap an App SDK instance (cookie-authenticated studio?). Pass one explicitly via the hook's `sdk` option, e.g. useWorkflowSession({engine, instanceId, sdk}).");
92
+ function bootstrapInstance(mounted, studio) {
156
93
  return createSanityInstance({
157
94
  projectId: mounted.projectId,
158
95
  dataset: mounted.dataset,
159
- auth: {
160
- token: sessionToken
161
- }
96
+ studio: studio
162
97
  });
163
98
  }
164
99
 
165
- function useDeferredDispose(instance) {
166
- const disposal = useRef(void 0);
100
+ function useSharedDispose(owner, shared) {
167
101
  useEffect(() => {
168
- if (instance !== void 0) return disposal.current?.instance === instance && (clearTimeout(disposal.current.timeoutId),
169
- disposal.current = void 0), () => {
170
- disposal.current = {
171
- instance: instance,
172
- timeoutId: setTimeout(() => {
173
- instance.isDisposed() || instance.dispose();
174
- }, 0)
175
- };
102
+ if (shared !== void 0) return shared.users += 1, () => {
103
+ const byResource = sharedSdks.get(owner);
104
+ byResource?.get(shared.key) === shared && (shared.users -= 1, setTimeout(() => {
105
+ shared.users !== 0 || byResource.get(shared.key) !== shared || (shared.instance.isDisposed() || shared.instance.dispose(),
106
+ byResource.delete(shared.key), byResource.size === 0 && sharedSdks.delete(owner));
107
+ }, 0));
176
108
  };
177
- }, [ instance ]);
109
+ }, [ owner, shared ]);
178
110
  }
179
111
 
180
112
  function useWorkflowEngine({workflowResource: workflowResource, tag: tag, resourceClients: resourceClients, effectHandlers: effectHandlers, missingHandler: missingHandler}) {
@@ -258,4 +190,145 @@ function useDocumentWorkflows({engine: engine, document: document, sdk: sdk}) {
258
190
  });
259
191
  }
260
192
 
261
- export { makeStudioObserver, studioResourceClients, useDocumentWorkflows, useStudioObserver, useWorkflowEngine, useWorkflowInstances, useWorkflowSession };
193
+ const PROJECT_USERS_TTL_MS = 3e4;
194
+
195
+ function isRecord(value) {
196
+ return typeof value == "object" && value !== null && !Array.isArray(value);
197
+ }
198
+
199
+ function isRole(value) {
200
+ return isRecord(value) && typeof value.name == "string";
201
+ }
202
+
203
+ function isMembership(value) {
204
+ return isRecord(value) && typeof value.id == "string" && (value.isRobot === void 0 || typeof value.isRobot == "boolean") && (value.roles === void 0 || Array.isArray(value.roles) && value.roles.every(isRole));
205
+ }
206
+
207
+ function isUser(value) {
208
+ return isRecord(value) && typeof value.id == "string" && (value.email === void 0 || typeof value.email == "string") && (value.displayName === void 0 || typeof value.displayName == "string") && (value.imageUrl === void 0 || value.imageUrl === null || typeof value.imageUrl == "string");
209
+ }
210
+
211
+ async function fetchProjectUsers(client, projectId) {
212
+ const project = await client.request({
213
+ uri: `/projects/${projectId}`
214
+ });
215
+ if (!isRecord(project) || !Array.isArray(project.members)) throw new Error("Studio project user response did not contain a members array");
216
+ const membershipRows = project.members;
217
+ if (!membershipRows.every(isMembership)) throw new Error("Studio project user response contained an invalid membership row");
218
+ const memberships = membershipRows.filter(member => !member.isRobot);
219
+ if (memberships.length === 0) return [];
220
+ const ids = memberships.map(member => encodeURIComponent(member.id)), response = await client.request({
221
+ uri: `/projects/${projectId}/users/${ids.join(",")}`
222
+ }), profiles = (Array.isArray(response) ? response : [ response ]).filter(isUser), profileById = new Map(profiles.map(profile => [ profile.id, profile ]));
223
+ return memberships.map(membership => ({
224
+ membership: membership,
225
+ profile: profileById.get(membership.id)
226
+ }));
227
+ }
228
+
229
+ const projectUsersByClient = /* @__PURE__ */ new WeakMap;
230
+
231
+ function clientProjectUsers(client) {
232
+ const cached = projectUsersByClient.get(client);
233
+ if (cached) return cached;
234
+ const created = /* @__PURE__ */ new Map;
235
+ return projectUsersByClient.set(client, created), created;
236
+ }
237
+
238
+ function loadProjectUsers(client, projectId) {
239
+ const projectUsers = clientProjectUsers(client), cached = projectUsers.get(projectId);
240
+ if (cached && cached.expiresAt > Date.now()) return cached.promise;
241
+ projectUsers.delete(projectId);
242
+ const pending = fetchProjectUsers(client, projectId), entry = {
243
+ promise: pending,
244
+ expiresAt: Number.POSITIVE_INFINITY
245
+ };
246
+ return projectUsers.set(projectId, entry), pending.then(() => {
247
+ entry.expiresAt = Date.now() + PROJECT_USERS_TTL_MS;
248
+ }, () => {
249
+ projectUsers.get(projectId) === entry && projectUsers.delete(projectId);
250
+ }), pending;
251
+ }
252
+
253
+ function studioProjectUserDirectory(client, projectId) {
254
+ const load = () => loadProjectUsers(client, projectId);
255
+ return {
256
+ findById: async id => {
257
+ try {
258
+ const user = (await load()).find(candidate => candidate.membership.id === id);
259
+ return user ? {
260
+ status: "resolved",
261
+ user: user
262
+ } : {
263
+ status: "missing"
264
+ };
265
+ } catch (cause) {
266
+ return {
267
+ status: "inaccessible",
268
+ cause: cause
269
+ };
270
+ }
271
+ },
272
+ findByEmail: async email => {
273
+ try {
274
+ const normalized = email.toLowerCase(), user = (await load()).find(candidate => candidate.profile?.email?.toLowerCase() === normalized);
275
+ return user ? {
276
+ status: "resolved",
277
+ user: user
278
+ } : {
279
+ status: "missing"
280
+ };
281
+ } catch (cause) {
282
+ return {
283
+ status: "inaccessible",
284
+ cause: cause
285
+ };
286
+ }
287
+ }
288
+ };
289
+ }
290
+
291
+ function useStudioProjectUsers() {
292
+ const client = useClient({
293
+ apiVersion: ENGINE_API_VERSION
294
+ }), projectId = client.config().projectId, directory = useMemo(() => projectId ? studioProjectUserDirectory(client, projectId) : void 0, [ client, projectId ]), [state, setState] = useState({
295
+ users: [],
296
+ loading: projectId !== void 0,
297
+ error: projectId === void 0 ? new Error("No projectId on the Studio client") : void 0
298
+ });
299
+ return useEffect(() => {
300
+ if (!projectId) {
301
+ setState({
302
+ users: [],
303
+ loading: !1,
304
+ error: new Error("No projectId on the Studio client")
305
+ });
306
+ return;
307
+ }
308
+ let cancelled = !1;
309
+ return setState({
310
+ users: [],
311
+ loading: !0,
312
+ error: void 0
313
+ }), loadProjectUsers(client, projectId).then(users => {
314
+ cancelled || setState({
315
+ users: users,
316
+ loading: !1,
317
+ error: void 0
318
+ });
319
+ }, error => {
320
+ cancelled || setState({
321
+ users: [],
322
+ loading: !1,
323
+ error: error
324
+ });
325
+ }), () => {
326
+ cancelled = !0;
327
+ };
328
+ }, [ client, projectId ]), {
329
+ ...state,
330
+ directory: directory
331
+ };
332
+ }
333
+
334
+ export { makeStudioObserver, studioProjectUserDirectory, studioResourceClients, useDocumentWorkflows, useStudioObserver, useStudioProjectUsers, useWorkflowEngine, useWorkflowInstances, useWorkflowSession };
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sanity/workflow-studio",
3
- "version": "0.8.0",
4
- "description": "React adapter that drives the @sanity/workflow-engine reactive session from the Sanity Studio document store.",
3
+ "version": "0.9.0",
4
+ "description": "React adapter that drives the @sanity/workflow-engine reactive session in Sanity Studio through the App SDK store.",
5
5
  "keywords": [
6
6
  "sanity",
7
7
  "sanity-io",
@@ -41,9 +41,9 @@
41
41
  "access": "public"
42
42
  },
43
43
  "dependencies": {
44
- "@sanity/workflow-engine": "0.17.0",
45
- "@sanity/workflow-react": "0.12.0",
46
- "@sanity/workflow-sdk": "0.9.0"
44
+ "@sanity/workflow-engine": "0.18.0",
45
+ "@sanity/workflow-react": "0.13.0",
46
+ "@sanity/workflow-sdk": "0.10.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@sanity/pkg-utils": "^10.5.2",
@@ -53,14 +53,13 @@
53
53
  "jsdom": "^29.1.1",
54
54
  "react": "^19.2.7",
55
55
  "react-dom": "^19.2.7",
56
- "sanity": "^5.30.0",
57
- "vitest": "^4.1.8",
58
- "@sanity/workflow-engine-test": "0.12.0"
56
+ "sanity": "^6",
57
+ "vitest": "^4.1.8"
59
58
  },
60
59
  "peerDependencies": {
61
60
  "@sanity/sdk": "^2.12.0",
62
61
  "react": "^19.2.7",
63
- "sanity": "^5.30.0"
62
+ "sanity": "^6"
64
63
  },
65
64
  "engines": {
66
65
  "node": ">=20"