@sanity/sdk 2.19.0 → 2.20.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/_internal.d.ts +2 -2
- package/dist/_exports/_internal.js +196 -22
- package/dist/_exports/_internal.js.map +1 -1
- package/dist/authStore-CWTAuieY.js +1072 -0
- package/dist/authStore-CWTAuieY.js.map +1 -0
- package/dist/clientStore-CMzJ7XhZ.js +168 -0
- package/dist/clientStore-CMzJ7XhZ.js.map +1 -0
- package/dist/createGroqSearchFilter-B0NdA-EP.js +1222 -0
- package/dist/createGroqSearchFilter-B0NdA-EP.js.map +1 -0
- package/dist/getEnv-76AjZd_f.js +178 -0
- package/dist/getEnv-76AjZd_f.js.map +1 -0
- package/dist/index.d.ts +2 -2105
- package/dist/index.js +4140 -3870
- package/dist/index.js.map +1 -1
- package/dist/rolldown-runtime-vyAXikos.js +9 -0
- package/dist/telemetryManager-Bd3O8z2S.js +127 -0
- package/dist/telemetryManager-Bd3O8z2S.js.map +1 -0
- package/dist/utils-BMwA8Kg9.d.ts +3334 -0
- package/dist/utils-BMwA8Kg9.d.ts.map +1 -0
- package/dist/version-DL2l07HG.js +9 -0
- package/dist/version-DL2l07HG.js.map +1 -0
- package/package.json +12 -20
- package/src/_exports/_internal.ts +1 -0
- package/src/_exports/index.ts +30 -0
- package/src/comments/addonDatasetStore.test.ts +215 -0
- package/src/comments/addonDatasetStore.ts +249 -0
- package/src/comments/buildCommentThreads.test.ts +83 -0
- package/src/comments/buildCommentThreads.ts +54 -0
- package/src/comments/commentActions.test.ts +540 -0
- package/src/comments/commentActions.ts +555 -0
- package/src/comments/commentFieldPath.test.ts +40 -0
- package/src/comments/commentFieldPath.ts +44 -0
- package/src/comments/commentsConstants.ts +80 -0
- package/src/comments/commentsStore.test.ts +419 -0
- package/src/comments/commentsStore.ts +483 -0
- package/src/comments/normalizeComment.test.ts +116 -0
- package/src/comments/normalizeComment.ts +45 -0
- package/src/comments/observeComments.test.ts +193 -0
- package/src/comments/observeComments.ts +113 -0
- package/src/comments/reducers.test.ts +277 -0
- package/src/comments/reducers.ts +316 -0
- package/src/comments/types.ts +249 -0
- package/src/comments/weakenReferences.test.ts +47 -0
- package/src/comments/weakenReferences.ts +29 -0
- package/src/document/documentConstants.ts +8 -0
- package/src/document/documentStore.test.ts +98 -1
- package/src/document/documentStore.ts +36 -5
- package/src/document/patchOperations.test.ts +27 -0
- package/src/document/patchOperations.ts +10 -1
- package/src/document/permissions.test.ts +3 -1
- package/src/document/reducers.test.ts +133 -0
- package/src/document/reducers.ts +18 -3
- package/src/projection/projectionStore.test.ts +0 -8
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts +0 -923
- package/dist/_chunks-dts/createGroqSearchFilter.d.ts.map +0 -1
- package/dist/_chunks-es/_internal.js +0 -134
- package/dist/_chunks-es/_internal.js.map +0 -1
- package/dist/_chunks-es/createGroqSearchFilter.js +0 -1590
- package/dist/_chunks-es/createGroqSearchFilter.js.map +0 -1
- package/dist/_chunks-es/telemetryManager.js +0 -93
- package/dist/_chunks-es/telemetryManager.js.map +0 -1
- package/dist/index.d.ts.map +0 -1
|
@@ -0,0 +1,1222 @@
|
|
|
1
|
+
import { A as defineStore, D as bindActionByResource, E as createStateSourceAction, P as isDatasetResource, j as isReleasePerspective } from "./authStore-CWTAuieY.js";
|
|
2
|
+
import { a as isObject, n as getClient, o as omitProperty, r as getClientState } from "./clientStore-CMzJ7XhZ.js";
|
|
3
|
+
import { ConnectionFailedError, CorsOriginError, DisconnectError } from "@sanity/client";
|
|
4
|
+
import { BehaviorSubject, EMPTY, NEVER, Observable, catchError, combineLatest, defer, distinctUntilChanged, filter, first, firstValueFrom, groupBy, map, mergeMap, of, pairwise, race, retry, share, startWith, switchMap, take, tap } from "rxjs";
|
|
5
|
+
import "@sanity/types";
|
|
6
|
+
import { createSelector } from "reselect";
|
|
7
|
+
import { DocumentId, getPublishedId } from "@sanity/id-utils";
|
|
8
|
+
import { stringifyPath } from "@sanity/json-match";
|
|
9
|
+
import "groq";
|
|
10
|
+
import { createImageUrlBuilder } from "@sanity/image-url";
|
|
11
|
+
function insecureRandomId() {
|
|
12
|
+
return Array.from({ length: 16 }, () => Math.floor(Math.random() * 16).toString(16)).join("");
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Like `setTimeout`, but calls `.unref()` on the timer when running in Node.js.
|
|
16
|
+
*
|
|
17
|
+
* In Node.js, active timers prevent the process from exiting. Cleanup timers
|
|
18
|
+
* (e.g. deferred subscription removal) should not keep the process alive -
|
|
19
|
+
* `.unref()` lets the process exit naturally while still firing the timer if
|
|
20
|
+
* the process happens to still be running.
|
|
21
|
+
*
|
|
22
|
+
* In browsers, `setTimeout` returns a number and has no `.unref()` method,
|
|
23
|
+
* so this is a no-op there.
|
|
24
|
+
*/
|
|
25
|
+
function setCleanupTimeout(fn, delay) {
|
|
26
|
+
let timer = setTimeout(fn, delay), t = timer;
|
|
27
|
+
return typeof t == "object" && t && "unref" in t && typeof t.unref == "function" && t.unref(), timer;
|
|
28
|
+
}
|
|
29
|
+
/** Data API version used for reading and writing comments in the addon dataset. */
|
|
30
|
+
const COMMENTS_API_VERSION = "v2025-05-06", ADDON_DATASET_API_VERSION = "v2025-02-19", COMMENTS_STATE_CLEAR_DELAY = 5e3, LISTEN_OPTIONS = {
|
|
31
|
+
events: [
|
|
32
|
+
"welcome",
|
|
33
|
+
"mutation",
|
|
34
|
+
"reconnect"
|
|
35
|
+
],
|
|
36
|
+
includeResult: !0,
|
|
37
|
+
includeAllVersions: !0,
|
|
38
|
+
visibility: "query",
|
|
39
|
+
tag: "comments.listen"
|
|
40
|
+
}, BASE_FILTERS = ["_type == \"comment\"", "target.document._ref == $documentId"];
|
|
41
|
+
/**
|
|
42
|
+
* Which comments belong to a document, matching the Studio's filter.
|
|
43
|
+
*
|
|
44
|
+
* A release's comments are kept apart from the default ones: asking for a
|
|
45
|
+
* release returns only that release's comments, and asking without one returns
|
|
46
|
+
* only comments with no version at all.
|
|
47
|
+
*/
|
|
48
|
+
function buildCommentsFilter(documentVersionId) {
|
|
49
|
+
return [...BASE_FILTERS, documentVersionId ? "target.documentVersionId == $documentVersionId" : "!defined(target.documentVersionId)"].join(" && ");
|
|
50
|
+
}
|
|
51
|
+
/** The snapshot query. Newest comment first, as the Studio orders it. */
|
|
52
|
+
function buildCommentsQuery(documentVersionId) {
|
|
53
|
+
return `*[${buildCommentsFilter(documentVersionId)}] {
|
|
54
|
+
_createdAt,
|
|
55
|
+
_id,
|
|
56
|
+
_rev,
|
|
57
|
+
authorId,
|
|
58
|
+
contentSnapshot,
|
|
59
|
+
context,
|
|
60
|
+
lastEditedAt,
|
|
61
|
+
message,
|
|
62
|
+
parentCommentId,
|
|
63
|
+
reactions,
|
|
64
|
+
status,
|
|
65
|
+
target,
|
|
66
|
+
threadId
|
|
67
|
+
} | order(_createdAt desc)`;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The listener query. Unprojected on purpose: mutation events carry the whole
|
|
71
|
+
* comment, so there is nothing to trim.
|
|
72
|
+
*/
|
|
73
|
+
function buildCommentsListenQuery(documentVersionId) {
|
|
74
|
+
return `*[${buildCommentsFilter(documentVersionId)}]`;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Comments need a project and a dataset. There is nowhere to put them for a
|
|
78
|
+
* media library or a canvas, and no Studio to interoperate with either.
|
|
79
|
+
*
|
|
80
|
+
* @internal
|
|
81
|
+
*/
|
|
82
|
+
function assertDatasetResource(resource) {
|
|
83
|
+
if (!isDatasetResource(resource)) throw Error(`Comments are only supported for dataset resources, received: ${JSON.stringify(resource)}`);
|
|
84
|
+
return resource;
|
|
85
|
+
}
|
|
86
|
+
function isMissingOrForbidden(error) {
|
|
87
|
+
if (typeof error != "object" || !error || !("statusCode" in error)) return !1;
|
|
88
|
+
let { statusCode } = error;
|
|
89
|
+
return statusCode === 403 || statusCode === 404;
|
|
90
|
+
}
|
|
91
|
+
function requestAddonDataset(client, { projectId, dataset }) {
|
|
92
|
+
return client.observable.request({
|
|
93
|
+
uri: `/projects/${projectId}/datasets?datasetProfile=comments&addonFor=${dataset}`,
|
|
94
|
+
tag: "comments.addon-dataset.list"
|
|
95
|
+
}).pipe(map((datasets) => datasets?.[0]?.name));
|
|
96
|
+
}
|
|
97
|
+
function discover(instance, resource) {
|
|
98
|
+
return getClientState(instance, {
|
|
99
|
+
apiVersion: ADDON_DATASET_API_VERSION,
|
|
100
|
+
projectId: resource.projectId,
|
|
101
|
+
useProjectHostname: !0
|
|
102
|
+
}).observable.pipe(switchMap((client) => requestAddonDataset(client, resource).pipe(catchError((error) => isMissingOrForbidden(error) ? of(void 0) : EMPTY))));
|
|
103
|
+
}
|
|
104
|
+
async function discoverOnce(instance, resource) {
|
|
105
|
+
let client = await firstValueFrom(getClientState(instance, {
|
|
106
|
+
apiVersion: ADDON_DATASET_API_VERSION,
|
|
107
|
+
projectId: resource.projectId,
|
|
108
|
+
useProjectHostname: !0
|
|
109
|
+
}).observable.pipe(take(1)));
|
|
110
|
+
try {
|
|
111
|
+
return await firstValueFrom(requestAddonDataset(client, resource));
|
|
112
|
+
} catch (error) {
|
|
113
|
+
if (isMissingOrForbidden(error)) return;
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
const addonDatasetStore = defineStore({
|
|
118
|
+
name: "CommentsAddonDataset",
|
|
119
|
+
getInitialState: () => ({ status: "unknown" }),
|
|
120
|
+
initialize: ({ instance, state, key }) => {
|
|
121
|
+
let subscription = discover(instance, assertDatasetResource(key.resource)).subscribe((datasetName) => state.set("setAddonDataset", datasetName ? {
|
|
122
|
+
status: "resolved",
|
|
123
|
+
datasetName
|
|
124
|
+
} : { status: "missing" }));
|
|
125
|
+
return () => subscription.unsubscribe();
|
|
126
|
+
}
|
|
127
|
+
}), getAddonDatasetState = bindActionByResource(addonDatasetStore, createStateSourceAction(({ state }) => {
|
|
128
|
+
if (state.status !== "unknown") return state.datasetName ?? null;
|
|
129
|
+
})), provisionAddonDataset = bindActionByResource(addonDatasetStore, ({ instance, state, key }) => {
|
|
130
|
+
let current = state.get();
|
|
131
|
+
if (current.datasetName) return Promise.resolve(current.datasetName);
|
|
132
|
+
if (current.provisioning) return current.provisioning;
|
|
133
|
+
let resource = assertDatasetResource(key.resource), provisioning = (async () => {
|
|
134
|
+
let existing = await discoverOnce(instance, resource);
|
|
135
|
+
if (existing) return existing;
|
|
136
|
+
let response = await getClient(instance, {
|
|
137
|
+
apiVersion: ADDON_DATASET_API_VERSION,
|
|
138
|
+
projectId: resource.projectId,
|
|
139
|
+
useProjectHostname: !0
|
|
140
|
+
}).request({
|
|
141
|
+
uri: `/comments/${resource.dataset}/setup`,
|
|
142
|
+
method: "POST",
|
|
143
|
+
tag: "comments.addon-dataset.setup"
|
|
144
|
+
});
|
|
145
|
+
if (!response?.datasetName) throw Error("Creating the comments addon dataset returned no dataset name.");
|
|
146
|
+
return response.datasetName;
|
|
147
|
+
})().then((datasetName) => (state.set("setAddonDataset", {
|
|
148
|
+
status: "resolved",
|
|
149
|
+
datasetName,
|
|
150
|
+
provisioning: void 0
|
|
151
|
+
}), datasetName)).catch((error) => {
|
|
152
|
+
throw state.set("clearProvisioning", { provisioning: void 0 }), error;
|
|
153
|
+
});
|
|
154
|
+
return state.set("startProvisioning", { provisioning }), provisioning;
|
|
155
|
+
}), observeAddonDatasetClient = bindActionByResource(addonDatasetStore, ({ instance, key }) => {
|
|
156
|
+
let { projectId } = assertDatasetResource(key.resource);
|
|
157
|
+
return getAddonDatasetState(instance, { resource: key.resource }).observable.pipe(distinctUntilChanged(), filter((datasetName) => datasetName !== void 0), switchMap((datasetName) => typeof datasetName == "string" ? getClientState(instance, {
|
|
158
|
+
apiVersion: COMMENTS_API_VERSION,
|
|
159
|
+
projectId,
|
|
160
|
+
dataset: datasetName
|
|
161
|
+
}).observable : of(null)));
|
|
162
|
+
}), RESERVED_SEGMENT = /\.(true|false|null)(?=$|[.[])/g, QUOTED_OR_PLAIN = /"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*'|[^"']+/g;
|
|
163
|
+
function quoteReservedFieldSegments(path) {
|
|
164
|
+
return path.replace(QUOTED_OR_PLAIN, (chunk) => chunk.startsWith("\"") || chunk.startsWith("'") ? chunk : chunk.replace(RESERVED_SEGMENT, "[\"$1\"]"));
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Normalises a field path to the string form comments are stored in.
|
|
168
|
+
*
|
|
169
|
+
* Comments keep `target.path.field` as a string rather than a path array,
|
|
170
|
+
* because that is what the Studio writes and reading it back has to match
|
|
171
|
+
* exactly. A string given here is parsed and re-stringified, so both a path
|
|
172
|
+
* array and a hand-written string come out in the same canonical form.
|
|
173
|
+
*
|
|
174
|
+
* `undefined` and `[]` both produce `''`. Callers writing a comment reject that
|
|
175
|
+
* rather than passing it on, since a comment has to point at a field.
|
|
176
|
+
*
|
|
177
|
+
* @example
|
|
178
|
+
* ```ts
|
|
179
|
+
* toCommentFieldPath(['body', {_key: 'intro'}, 'content'])
|
|
180
|
+
* // 'body[_key=="intro"].content'
|
|
181
|
+
* toCommentFieldPath(undefined)
|
|
182
|
+
* // ''
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* @internal
|
|
186
|
+
*/
|
|
187
|
+
function toCommentFieldPath(path) {
|
|
188
|
+
return quoteReservedFieldSegments(stringifyPath(path));
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Groups a flat comment list into threads.
|
|
192
|
+
*
|
|
193
|
+
* A thread's parent is the comment with no `parentCommentId`; replies point at
|
|
194
|
+
* the parent's `id`. Replies whose parent is missing from the input are
|
|
195
|
+
* dropped, which is what happens when a parent is deleted but its replies have
|
|
196
|
+
* not disappeared yet.
|
|
197
|
+
*
|
|
198
|
+
* Threads come back in the order their parents appear in `comments`, so passing
|
|
199
|
+
* the store's newest-first list gives newest-thread-first. Replies within a
|
|
200
|
+
* thread are always oldest first, since that is reading order.
|
|
201
|
+
*
|
|
202
|
+
* @internal
|
|
203
|
+
*/
|
|
204
|
+
function buildCommentThreads(comments) {
|
|
205
|
+
let repliesByParent = /* @__PURE__ */ new Map();
|
|
206
|
+
for (let comment of comments) {
|
|
207
|
+
if (!comment.parentCommentId) continue;
|
|
208
|
+
let existing = repliesByParent.get(comment.parentCommentId);
|
|
209
|
+
existing ? existing.push(comment) : repliesByParent.set(comment.parentCommentId, [comment]);
|
|
210
|
+
}
|
|
211
|
+
let threads = [];
|
|
212
|
+
for (let parentComment of comments) {
|
|
213
|
+
if (parentComment.parentCommentId) continue;
|
|
214
|
+
let replies = (repliesByParent.get(parentComment.id) ?? []).sort((a, b) => a.createdAt.localeCompare(b.createdAt)), lastReply = replies[replies.length - 1];
|
|
215
|
+
threads.push({
|
|
216
|
+
threadId: parentComment.threadId,
|
|
217
|
+
fieldPath: parentComment.fieldPath,
|
|
218
|
+
parentComment,
|
|
219
|
+
replies,
|
|
220
|
+
commentsCount: replies.length + 1,
|
|
221
|
+
status: parentComment.status,
|
|
222
|
+
lastActivityAt: lastReply ? lastReply.createdAt : parentComment.createdAt
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
return threads;
|
|
226
|
+
}
|
|
227
|
+
/**
|
|
228
|
+
* Turns a stored comment into the shape consumers see.
|
|
229
|
+
*
|
|
230
|
+
* This is the whole seam between the storage format and the public API. When
|
|
231
|
+
* comments move to the organization-level store, the three fields that differ
|
|
232
|
+
* are handled here and nothing downstream changes:
|
|
233
|
+
*
|
|
234
|
+
* - the author moves from `authorId` to `_system.createdBy`
|
|
235
|
+
* - `target.document` becomes a global document reference, so `documentId` is
|
|
236
|
+
* parsed out of a `resourceType:resourceId:documentId` string rather than
|
|
237
|
+
* read off `_ref`
|
|
238
|
+
* - `_type` goes from `comment` to `sanity.comment`, and stops being exposed
|
|
239
|
+
*
|
|
240
|
+
* @internal
|
|
241
|
+
*/
|
|
242
|
+
function normalizeComment(stored) {
|
|
243
|
+
let { target } = stored;
|
|
244
|
+
return {
|
|
245
|
+
id: stored._id,
|
|
246
|
+
createdAt: stored._createdAt,
|
|
247
|
+
...stored.authorId ? { authorId: stored.authorId } : {},
|
|
248
|
+
message: stored.message,
|
|
249
|
+
threadId: stored.threadId,
|
|
250
|
+
...stored.parentCommentId ? { parentCommentId: stored.parentCommentId } : {},
|
|
251
|
+
status: stored.status,
|
|
252
|
+
...stored.lastEditedAt ? { lastEditedAt: stored.lastEditedAt } : {},
|
|
253
|
+
documentId: target.document._ref,
|
|
254
|
+
documentType: target.documentType,
|
|
255
|
+
fieldPath: target.path?.field ?? "",
|
|
256
|
+
...target.path?.selection ? { selection: target.path.selection } : {},
|
|
257
|
+
...stored.contentSnapshot === void 0 ? {} : { contentSnapshot: stored.contentSnapshot },
|
|
258
|
+
reactions: (stored.reactions ?? []).map(({ shortName, userId, addedAt }) => ({
|
|
259
|
+
shortName,
|
|
260
|
+
userId,
|
|
261
|
+
addedAt
|
|
262
|
+
})),
|
|
263
|
+
...stored._state ? { state: stored._state } : {}
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
function toCommentsEvent(event) {
|
|
267
|
+
if (event.type === "mutation") {
|
|
268
|
+
if (event.transition === "disappear") return {
|
|
269
|
+
type: "disappear",
|
|
270
|
+
commentId: event.documentId
|
|
271
|
+
};
|
|
272
|
+
if (event.result) return event.transition === "appear" ? {
|
|
273
|
+
type: "appear",
|
|
274
|
+
comment: event.result
|
|
275
|
+
} : {
|
|
276
|
+
type: "update",
|
|
277
|
+
comment: event.result,
|
|
278
|
+
transactionId: event.transactionId
|
|
279
|
+
};
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Watches one document's comments: a snapshot on connect, then live changes.
|
|
284
|
+
*
|
|
285
|
+
* The Studio awaits its snapshot inside the event handler, which loses any
|
|
286
|
+
* mutation that arrives while the fetch is in flight, because the older
|
|
287
|
+
* snapshot overwrites it. Here the mutations that arrive during a fetch are
|
|
288
|
+
* held and replayed straight after the snapshot, so nothing is dropped.
|
|
289
|
+
*
|
|
290
|
+
* A reconnect produces a fresh `welcome`, which cancels any fetch still running
|
|
291
|
+
* and starts a new one. Reconnects emit nothing themselves: dropping the list
|
|
292
|
+
* while reconnecting would only make readers suspend again for data we still
|
|
293
|
+
* have.
|
|
294
|
+
*
|
|
295
|
+
* @internal
|
|
296
|
+
*/
|
|
297
|
+
function observeComments(options) {
|
|
298
|
+
let { client, documentId, documentVersionId } = options, params = {
|
|
299
|
+
documentId,
|
|
300
|
+
...documentVersionId ? { documentVersionId } : {}
|
|
301
|
+
}, events$ = client.observable.listen(buildCommentsListenQuery(documentVersionId), params, LISTEN_OPTIONS).pipe(share()), mutations$ = events$.pipe(map(toCommentsEvent), filter((event) => event !== void 0));
|
|
302
|
+
return events$.pipe(filter((event) => event.type === "welcome"), switchMap(() => new Observable((observer) => {
|
|
303
|
+
let buffered = [], snapshotReceived = !1, mutationSubscription = mutations$.subscribe({
|
|
304
|
+
next: (event) => {
|
|
305
|
+
snapshotReceived ? observer.next(event) : buffered.push(event);
|
|
306
|
+
},
|
|
307
|
+
error: (error) => observer.error(error)
|
|
308
|
+
}), snapshotSubscription = client.observable.fetch(buildCommentsQuery(documentVersionId), params, { tag: "comments.list" }).pipe(take(1)).subscribe({
|
|
309
|
+
next: (comments) => {
|
|
310
|
+
observer.next({
|
|
311
|
+
type: "snapshot",
|
|
312
|
+
comments
|
|
313
|
+
}), snapshotReceived = !0;
|
|
314
|
+
for (let event of buffered) observer.next(event);
|
|
315
|
+
buffered.length = 0;
|
|
316
|
+
},
|
|
317
|
+
error: (error) => {
|
|
318
|
+
observer.next({
|
|
319
|
+
type: "error",
|
|
320
|
+
error
|
|
321
|
+
}), observer.complete();
|
|
322
|
+
}
|
|
323
|
+
});
|
|
324
|
+
return () => {
|
|
325
|
+
mutationSubscription.unsubscribe(), snapshotSubscription.unsubscribe(), buffered.length = 0;
|
|
326
|
+
};
|
|
327
|
+
})));
|
|
328
|
+
}
|
|
329
|
+
function getCommentsKey({ documentId, documentVersionId }) {
|
|
330
|
+
return JSON.stringify([documentId, documentVersionId ?? null]);
|
|
331
|
+
}
|
|
332
|
+
function parseCommentsKey(key) {
|
|
333
|
+
let [documentId, documentVersionId] = JSON.parse(key);
|
|
334
|
+
return {
|
|
335
|
+
documentId,
|
|
336
|
+
...documentVersionId ? { documentVersionId } : {}
|
|
337
|
+
};
|
|
338
|
+
}
|
|
339
|
+
const addSubscriber$1 = (key, subscriptionId) => (prev) => {
|
|
340
|
+
let entry = prev.entries[key], subscribers = [...entry?.subscribers ?? [], subscriptionId];
|
|
341
|
+
return {
|
|
342
|
+
...prev,
|
|
343
|
+
entries: {
|
|
344
|
+
...prev.entries,
|
|
345
|
+
[key]: {
|
|
346
|
+
...entry,
|
|
347
|
+
subscribers
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
};
|
|
351
|
+
}, removeSubscriber$1 = (key, subscriptionId) => (prev) => {
|
|
352
|
+
let entry = prev.entries[key];
|
|
353
|
+
if (!entry) return prev;
|
|
354
|
+
let subscribers = entry.subscribers.filter((id) => id !== subscriptionId);
|
|
355
|
+
if (!subscribers.length) {
|
|
356
|
+
let pendingCreates = { ...prev.pendingCreates }, pendingTransactions = { ...prev.pendingTransactions };
|
|
357
|
+
for (let commentId of Object.keys(entry.comments ?? {})) delete pendingCreates[commentId], delete pendingTransactions[commentId];
|
|
358
|
+
return {
|
|
359
|
+
...prev,
|
|
360
|
+
entries: omitProperty(prev.entries, key),
|
|
361
|
+
pendingCreates,
|
|
362
|
+
pendingTransactions
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
return {
|
|
366
|
+
...prev,
|
|
367
|
+
entries: {
|
|
368
|
+
...prev.entries,
|
|
369
|
+
[key]: {
|
|
370
|
+
...entry,
|
|
371
|
+
subscribers
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
}, setComments = (key, comments) => (prev) => {
|
|
376
|
+
let entry = prev.entries[key];
|
|
377
|
+
if (!entry) return prev;
|
|
378
|
+
let byId = Object.fromEntries(comments.map((comment) => [comment._id, comment])), pendingCreates = { ...prev.pendingCreates };
|
|
379
|
+
for (let [commentId, localComment] of Object.entries(entry.comments ?? {})) Object.hasOwn(byId, commentId) ? delete pendingCreates[commentId] : (Object.hasOwn(pendingCreates, commentId) || localComment._state) && (byId[commentId] = localComment);
|
|
380
|
+
return {
|
|
381
|
+
...prev,
|
|
382
|
+
entries: {
|
|
383
|
+
...prev.entries,
|
|
384
|
+
[key]: {
|
|
385
|
+
...entry,
|
|
386
|
+
comments: byId,
|
|
387
|
+
error: void 0
|
|
388
|
+
}
|
|
389
|
+
},
|
|
390
|
+
pendingCreates
|
|
391
|
+
};
|
|
392
|
+
}, setCommentsError = (key, error) => (prev) => {
|
|
393
|
+
let entry = prev.entries[key];
|
|
394
|
+
return entry ? {
|
|
395
|
+
...prev,
|
|
396
|
+
entries: {
|
|
397
|
+
...prev.entries,
|
|
398
|
+
[key]: {
|
|
399
|
+
...entry,
|
|
400
|
+
error
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
} : prev;
|
|
404
|
+
}, receiveComment = (key, comment) => (prev) => {
|
|
405
|
+
let entry = prev.entries[key], pendingCreates = omitProperty(prev.pendingCreates, comment._id);
|
|
406
|
+
return entry ? {
|
|
407
|
+
...prev,
|
|
408
|
+
pendingCreates,
|
|
409
|
+
entries: {
|
|
410
|
+
...prev.entries,
|
|
411
|
+
[key]: {
|
|
412
|
+
...entry,
|
|
413
|
+
comments: {
|
|
414
|
+
...entry.comments,
|
|
415
|
+
[comment._id]: comment
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
} : {
|
|
420
|
+
...prev,
|
|
421
|
+
pendingCreates
|
|
422
|
+
};
|
|
423
|
+
};
|
|
424
|
+
/** A create that already failed once is being retried, not written fresh. */
|
|
425
|
+
function isRetryingCreate(existing) {
|
|
426
|
+
let state = existing?._state?.type;
|
|
427
|
+
return state === "createError" || state === "createRetrying";
|
|
428
|
+
}
|
|
429
|
+
function mergeOptimisticComment(existing, comment) {
|
|
430
|
+
return {
|
|
431
|
+
...existing,
|
|
432
|
+
...comment,
|
|
433
|
+
_createdAt: comment._createdAt ?? existing?._createdAt ?? (/* @__PURE__ */ new Date()).toISOString(),
|
|
434
|
+
_rev: comment._rev ?? existing?._rev ?? "",
|
|
435
|
+
...isRetryingCreate(existing) ? { _state: { type: "createRetrying" } } : {}
|
|
436
|
+
};
|
|
437
|
+
}
|
|
438
|
+
const addComment = (key, comment) => (prev) => {
|
|
439
|
+
let entry = prev.entries[key];
|
|
440
|
+
if (!entry) return prev;
|
|
441
|
+
let existing = entry.comments && Object.hasOwn(entry.comments, comment._id) ? entry.comments[comment._id] : void 0;
|
|
442
|
+
return {
|
|
443
|
+
...prev,
|
|
444
|
+
pendingCreates: {
|
|
445
|
+
...prev.pendingCreates,
|
|
446
|
+
[comment._id]: !0
|
|
447
|
+
},
|
|
448
|
+
entries: {
|
|
449
|
+
...prev.entries,
|
|
450
|
+
[key]: {
|
|
451
|
+
...entry,
|
|
452
|
+
comments: {
|
|
453
|
+
...entry.comments,
|
|
454
|
+
[comment._id]: mergeOptimisticComment(existing, comment)
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
};
|
|
459
|
+
}, applyCommentUpdate = (commentId, patch) => (prev) => {
|
|
460
|
+
let entries = { ...prev.entries }, changed = !1;
|
|
461
|
+
for (let [key, entry] of Object.entries(prev.entries)) {
|
|
462
|
+
let comment = entry?.comments?.[commentId];
|
|
463
|
+
!entry || !comment || (changed = !0, entries[key] = {
|
|
464
|
+
...entry,
|
|
465
|
+
comments: {
|
|
466
|
+
...entry.comments,
|
|
467
|
+
[commentId]: {
|
|
468
|
+
...comment,
|
|
469
|
+
...patch
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return changed ? {
|
|
475
|
+
...prev,
|
|
476
|
+
entries
|
|
477
|
+
} : prev;
|
|
478
|
+
}, removeCommentById = (commentId) => (prev) => {
|
|
479
|
+
let entries = { ...prev.entries }, removedIds = /* @__PURE__ */ new Set(), changed = !1;
|
|
480
|
+
for (let [key, entry] of Object.entries(prev.entries)) {
|
|
481
|
+
if (!entry?.comments) continue;
|
|
482
|
+
let remaining = Object.fromEntries(Object.entries(entry.comments).filter(([id, comment]) => {
|
|
483
|
+
let keep = id !== commentId && comment.parentCommentId !== commentId;
|
|
484
|
+
return keep || removedIds.add(id), keep;
|
|
485
|
+
}));
|
|
486
|
+
Object.keys(remaining).length !== Object.keys(entry.comments).length && (changed = !0, entries[key] = {
|
|
487
|
+
...entry,
|
|
488
|
+
comments: remaining
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
if (!changed) return prev;
|
|
492
|
+
let pendingCreates = { ...prev.pendingCreates }, pendingTransactions = { ...prev.pendingTransactions };
|
|
493
|
+
for (let id of removedIds) delete pendingCreates[id], delete pendingTransactions[id];
|
|
494
|
+
return {
|
|
495
|
+
...prev,
|
|
496
|
+
entries,
|
|
497
|
+
pendingCreates,
|
|
498
|
+
pendingTransactions
|
|
499
|
+
};
|
|
500
|
+
}, setPendingTransaction = (commentId, transactionId) => (prev) => ({
|
|
501
|
+
...prev,
|
|
502
|
+
pendingTransactions: {
|
|
503
|
+
...prev.pendingTransactions,
|
|
504
|
+
[commentId]: transactionId
|
|
505
|
+
}
|
|
506
|
+
}), clearPendingTransaction = (commentId) => (prev) => ({
|
|
507
|
+
...prev,
|
|
508
|
+
pendingTransactions: omitProperty(prev.pendingTransactions, commentId)
|
|
509
|
+
}), setCommentCreateError = (commentId, error) => (prev) => {
|
|
510
|
+
if (!Object.hasOwn(prev.pendingCreates, commentId)) return prev;
|
|
511
|
+
let next = applyCommentUpdate(commentId, { _state: {
|
|
512
|
+
type: "createError",
|
|
513
|
+
error
|
|
514
|
+
} })(prev);
|
|
515
|
+
return {
|
|
516
|
+
...next,
|
|
517
|
+
pendingCreates: omitProperty(next.pendingCreates, commentId)
|
|
518
|
+
};
|
|
519
|
+
}, rollbackCommentUpdate = (commentId, transactionId, previous) => (prev) => {
|
|
520
|
+
if (!Object.hasOwn(prev.pendingTransactions, commentId) || prev.pendingTransactions[commentId] !== transactionId) return prev;
|
|
521
|
+
let entries = Object.fromEntries(Object.entries(prev.entries).map(([key, entry]) => entry?.comments?.[commentId] ? [key, {
|
|
522
|
+
...entry,
|
|
523
|
+
comments: {
|
|
524
|
+
...entry.comments,
|
|
525
|
+
[commentId]: previous
|
|
526
|
+
}
|
|
527
|
+
}] : [key, entry]));
|
|
528
|
+
return {
|
|
529
|
+
...prev,
|
|
530
|
+
entries,
|
|
531
|
+
pendingTransactions: omitProperty(prev.pendingTransactions, commentId)
|
|
532
|
+
};
|
|
533
|
+
}, restoreComments = (removed) => (prev) => {
|
|
534
|
+
let entries = { ...prev.entries }, changed = !1;
|
|
535
|
+
for (let { key, comments } of removed) {
|
|
536
|
+
let entry = entries[key];
|
|
537
|
+
if (!entry) continue;
|
|
538
|
+
let missing = comments.filter((comment) => !Object.hasOwn(entry.comments ?? {}, comment._id));
|
|
539
|
+
missing.length && (changed = !0, entries[key] = {
|
|
540
|
+
...entry,
|
|
541
|
+
comments: Object.fromEntries([...Object.entries(entry.comments ?? {}), ...missing.map((comment) => [comment._id, comment])])
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
return changed ? {
|
|
545
|
+
...prev,
|
|
546
|
+
entries
|
|
547
|
+
} : prev;
|
|
548
|
+
};
|
|
549
|
+
/**
|
|
550
|
+
* A stable string standing for one set of read options.
|
|
551
|
+
*
|
|
552
|
+
* Only React needs this: it holds one state source steady across renders and
|
|
553
|
+
* defers swapping to a new one while the previous list is still on screen.
|
|
554
|
+
* `fieldPath` is normalised on the way in, so a path array and the equivalent
|
|
555
|
+
* string address the same list.
|
|
556
|
+
*
|
|
557
|
+
* @internal
|
|
558
|
+
*/
|
|
559
|
+
function getCommentsOptionsKey(options) {
|
|
560
|
+
return JSON.stringify({
|
|
561
|
+
documentId: options.documentId,
|
|
562
|
+
documentType: options.documentType,
|
|
563
|
+
projectId: options.projectId,
|
|
564
|
+
dataset: options.dataset,
|
|
565
|
+
resource: options.resource,
|
|
566
|
+
perspective: options.perspective,
|
|
567
|
+
fieldPath: options.fieldPath === void 0 ? void 0 : toCommentFieldPath(options.fieldPath),
|
|
568
|
+
status: options.status
|
|
569
|
+
});
|
|
570
|
+
}
|
|
571
|
+
/** @internal */
|
|
572
|
+
function parseCommentsOptionsKey(key) {
|
|
573
|
+
return JSON.parse(key);
|
|
574
|
+
}
|
|
575
|
+
/**
|
|
576
|
+
* Which comment list an option set addresses.
|
|
577
|
+
*
|
|
578
|
+
* Comments hang off the published id, so a draft and its published document
|
|
579
|
+
* share one list. A release keeps its own.
|
|
580
|
+
*/
|
|
581
|
+
function toCommentsKeyParts(instance, options) {
|
|
582
|
+
let perspective = options.perspective ?? instance.config.perspective;
|
|
583
|
+
return {
|
|
584
|
+
documentId: getPublishedId(DocumentId(options.documentId)),
|
|
585
|
+
...isReleasePerspective(perspective) ? { documentVersionId: perspective.releaseName } : {}
|
|
586
|
+
};
|
|
587
|
+
}
|
|
588
|
+
function applyEvent(state, key, event) {
|
|
589
|
+
switch (event.type) {
|
|
590
|
+
case "snapshot":
|
|
591
|
+
state.set("setComments", setComments(key, event.comments));
|
|
592
|
+
return;
|
|
593
|
+
case "appear":
|
|
594
|
+
state.set("receiveComment", receiveComment(key, event.comment));
|
|
595
|
+
return;
|
|
596
|
+
case "disappear":
|
|
597
|
+
state.set("removeComment", removeCommentById(event.commentId));
|
|
598
|
+
return;
|
|
599
|
+
case "error":
|
|
600
|
+
state.set("setCommentsError", setCommentsError(key, event.error));
|
|
601
|
+
return;
|
|
602
|
+
case "update": {
|
|
603
|
+
let pending = state.get().pendingTransactions[event.comment._id];
|
|
604
|
+
if (pending && pending !== event.transactionId) return;
|
|
605
|
+
state.set("receiveComment", receiveComment(key, event.comment)), pending && state.set("clearPendingTransaction", clearPendingTransaction(event.comment._id));
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const watchSubscribedDocuments = ({ state, instance, key }) => {
|
|
610
|
+
let client$ = observeAddonDatasetClient(instance, { resource: key.resource });
|
|
611
|
+
return state.observable.pipe(map((current) => new Set(Object.keys(current.entries))), distinctUntilChanged((a, b) => a.size === b.size && Array.from(b).every((entry) => a.has(entry))), startWith(/* @__PURE__ */ new Set()), pairwise(), mergeMap(([previous, current]) => [...Array.from(current).filter((entry) => !previous.has(entry)).map((entry) => ({
|
|
612
|
+
key: entry,
|
|
613
|
+
added: !0
|
|
614
|
+
})), ...Array.from(previous).filter((entry) => !current.has(entry)).map((entry) => ({
|
|
615
|
+
key: entry,
|
|
616
|
+
added: !1
|
|
617
|
+
}))]), groupBy((event) => event.key), mergeMap((group$) => group$.pipe(switchMap((event) => {
|
|
618
|
+
if (!event.added) return EMPTY;
|
|
619
|
+
let { documentId, documentVersionId } = parseCommentsKey(group$.key);
|
|
620
|
+
return client$.pipe(switchMap((client) => client ? observeComments({
|
|
621
|
+
client,
|
|
622
|
+
documentId,
|
|
623
|
+
documentVersionId
|
|
624
|
+
}).pipe(tap((commentsEvent) => applyEvent(state, group$.key, commentsEvent)), catchError((error) => (state.set("setCommentsError", setCommentsError(group$.key, error)), EMPTY))) : (state.set("setComments", setComments(group$.key, [])), EMPTY)));
|
|
625
|
+
})))).subscribe({ error: (error) => state.set("setError", { error }) });
|
|
626
|
+
}, commentsStore = defineStore({
|
|
627
|
+
name: "Comments",
|
|
628
|
+
getInitialState: () => ({
|
|
629
|
+
entries: {},
|
|
630
|
+
pendingCreates: {},
|
|
631
|
+
pendingTransactions: {}
|
|
632
|
+
}),
|
|
633
|
+
initialize: (context) => {
|
|
634
|
+
let subscription = watchSubscribedDocuments(context);
|
|
635
|
+
return () => subscription.unsubscribe();
|
|
636
|
+
}
|
|
637
|
+
}), normalizedCache = /* @__PURE__ */ new WeakMap(), filteredCache = /* @__PURE__ */ new WeakMap(), threadCache = /* @__PURE__ */ new WeakMap();
|
|
638
|
+
/**
|
|
639
|
+
* The stored map turned into what consumers see, newest first.
|
|
640
|
+
*
|
|
641
|
+
* Cached alongside the filters below rather than done per read, so the
|
|
642
|
+
* normalised objects keep their identity for as long as the stored map does.
|
|
643
|
+
*/
|
|
644
|
+
function normalizeAll(commentsById) {
|
|
645
|
+
let cached = normalizedCache.get(commentsById);
|
|
646
|
+
if (cached) return cached;
|
|
647
|
+
let normalized = Object.values(commentsById).map(normalizeComment).sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
648
|
+
return normalizedCache.set(commentsById, normalized), normalized;
|
|
649
|
+
}
|
|
650
|
+
function filterComments(all, fieldPath, status) {
|
|
651
|
+
let byFilter = filteredCache.get(all);
|
|
652
|
+
byFilter || (byFilter = /* @__PURE__ */ new Map(), filteredCache.set(all, byFilter));
|
|
653
|
+
let cacheKey = `${fieldPath ?? "\0"}|${status ?? ""}`, cached = byFilter.get(cacheKey);
|
|
654
|
+
if (cached) return cached;
|
|
655
|
+
let filtered = all.filter((comment) => status && comment.status !== status ? !1 : fieldPath === void 0 || comment.fieldPath === fieldPath);
|
|
656
|
+
return byFilter.set(cacheKey, filtered), filtered;
|
|
657
|
+
}
|
|
658
|
+
function selectComments({ state, instance }, options) {
|
|
659
|
+
if (state.error) throw state.error;
|
|
660
|
+
let entry = state.entries[getCommentsKey(toCommentsKeyParts(instance, options))];
|
|
661
|
+
if (entry?.error) throw entry.error;
|
|
662
|
+
if (entry?.comments) return filterComments(normalizeAll(entry.comments), options.fieldPath === void 0 ? void 0 : toCommentFieldPath(options.fieldPath), options.status);
|
|
663
|
+
}
|
|
664
|
+
function selectCommentThreads(context, options) {
|
|
665
|
+
let comments = selectComments(context, {
|
|
666
|
+
...options,
|
|
667
|
+
fieldPath: void 0,
|
|
668
|
+
status: void 0
|
|
669
|
+
});
|
|
670
|
+
if (comments === void 0) return;
|
|
671
|
+
let byFilter = threadCache.get(comments);
|
|
672
|
+
byFilter || (byFilter = /* @__PURE__ */ new Map(), threadCache.set(comments, byFilter));
|
|
673
|
+
let fieldPath = options.fieldPath === void 0 ? void 0 : toCommentFieldPath(options.fieldPath), cacheKey = `${fieldPath ?? "\0"}|${options.status ?? ""}`, cached = byFilter.get(cacheKey);
|
|
674
|
+
if (cached) return cached;
|
|
675
|
+
let threads = buildCommentThreads(comments).filter((thread) => options.status && thread.parentComment.status !== options.status ? !1 : fieldPath === void 0 || thread.fieldPath === fieldPath);
|
|
676
|
+
return byFilter.set(cacheKey, threads), threads;
|
|
677
|
+
}
|
|
678
|
+
/**
|
|
679
|
+
* Every comment on a document, newest first.
|
|
680
|
+
*
|
|
681
|
+
* `undefined` until the first snapshot arrives. Replies are included; use
|
|
682
|
+
* {@link getCommentThreadsState} to read them grouped.
|
|
683
|
+
*
|
|
684
|
+
* @beta
|
|
685
|
+
*/
|
|
686
|
+
const getCommentsState = bindActionByResource(commentsStore, createStateSourceAction({
|
|
687
|
+
selector: selectComments,
|
|
688
|
+
onSubscribe: ({ state, instance }, options) => {
|
|
689
|
+
let key = getCommentsKey(toCommentsKeyParts(instance, options)), subscriptionId = insecureRandomId();
|
|
690
|
+
return state.set("addSubscriber", addSubscriber$1(key, subscriptionId)), () => {
|
|
691
|
+
setCleanupTimeout(() => state.set("removeSubscriber", removeSubscriber$1(key, subscriptionId)), COMMENTS_STATE_CLEAR_DELAY);
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
})), getCommentThreadsState = bindActionByResource(commentsStore, createStateSourceAction({
|
|
695
|
+
selector: selectCommentThreads,
|
|
696
|
+
onSubscribe: ({ state, instance }, options) => {
|
|
697
|
+
let key = getCommentsKey(toCommentsKeyParts(instance, options)), subscriptionId = insecureRandomId();
|
|
698
|
+
return state.set("addSubscriber", addSubscriber$1(key, subscriptionId)), () => {
|
|
699
|
+
setCleanupTimeout(() => state.set("removeSubscriber", removeSubscriber$1(key, subscriptionId)), COMMENTS_STATE_CLEAR_DELAY);
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
})), resolveComments = bindActionByResource(commentsStore, ({ state, instance }, { signal, ...options }) => resolveList(state, instance, options, signal, getCommentsState)), resolveCommentThreads = bindActionByResource(commentsStore, ({ state, instance }, { signal, ...options }) => resolveList(state, instance, options, signal, getCommentThreadsState));
|
|
703
|
+
function resolveList(state, instance, options, signal, getState) {
|
|
704
|
+
let key = getCommentsKey(toCommentsKeyParts(instance, options)), { getCurrent } = getState(instance, options), subscriptionId = insecureRandomId();
|
|
705
|
+
state.set("addSubscriber", addSubscriber$1(key, subscriptionId));
|
|
706
|
+
let release = () => state.set("removeSubscriber", removeSubscriber$1(key, subscriptionId)), aborted$ = signal ? new Observable((observer) => {
|
|
707
|
+
let listener = () => {
|
|
708
|
+
release(), observer.error(new DOMException("The operation was aborted.", "AbortError"));
|
|
709
|
+
};
|
|
710
|
+
return signal.addEventListener("abort", listener), () => signal.removeEventListener("abort", listener);
|
|
711
|
+
}) : NEVER, resolved$ = state.observable.pipe(map(() => getCurrent()), first((value) => value !== void 0)), promise = firstValueFrom(race([resolved$, aborted$])), releaseLater = () => setCleanupTimeout(release, COMMENTS_STATE_CLEAR_DELAY);
|
|
712
|
+
return promise.then(releaseLater, releaseLater), promise;
|
|
713
|
+
}
|
|
714
|
+
const PROJECT_API_VERSION = "2025-07-18", USERS_STATE_CLEAR_DELAY = 5e3, getUsersKey = (instance, { resourceType, organizationId, batchSize = 100, projectId = instance.config.projectId, userId } = {}) => JSON.stringify({
|
|
715
|
+
resourceType,
|
|
716
|
+
organizationId,
|
|
717
|
+
batchSize,
|
|
718
|
+
projectId,
|
|
719
|
+
userId
|
|
720
|
+
}), parseUsersKey = (key) => JSON.parse(key), addSubscription = (subscriptionId, key) => (prev) => {
|
|
721
|
+
let group = prev.users[key], subscriptions = [...group?.subscriptions ?? [], subscriptionId];
|
|
722
|
+
return {
|
|
723
|
+
...prev,
|
|
724
|
+
users: {
|
|
725
|
+
...prev.users,
|
|
726
|
+
[key]: {
|
|
727
|
+
...group,
|
|
728
|
+
subscriptions
|
|
729
|
+
}
|
|
730
|
+
}
|
|
731
|
+
};
|
|
732
|
+
}, removeSubscription = (subscriptionId, key) => (prev) => {
|
|
733
|
+
let group = prev.users[key];
|
|
734
|
+
if (!group) return prev;
|
|
735
|
+
let subscriptions = group.subscriptions.filter((id) => id !== subscriptionId);
|
|
736
|
+
return subscriptions.length ? {
|
|
737
|
+
...prev,
|
|
738
|
+
users: {
|
|
739
|
+
...prev.users,
|
|
740
|
+
[key]: {
|
|
741
|
+
...group,
|
|
742
|
+
subscriptions
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
} : {
|
|
746
|
+
...prev,
|
|
747
|
+
users: omitProperty(prev.users, key)
|
|
748
|
+
};
|
|
749
|
+
}, setUsersData = (key, { data, nextCursor, totalCount }) => (prev) => {
|
|
750
|
+
let group = prev.users[key];
|
|
751
|
+
if (!group) return prev;
|
|
752
|
+
let users = [...group.users ?? [], ...data];
|
|
753
|
+
return {
|
|
754
|
+
...prev,
|
|
755
|
+
users: {
|
|
756
|
+
...prev.users,
|
|
757
|
+
[key]: {
|
|
758
|
+
...group,
|
|
759
|
+
users,
|
|
760
|
+
totalCount,
|
|
761
|
+
nextCursor
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
};
|
|
765
|
+
}, updateLastLoadMoreRequest = (timestamp, key) => (prev) => {
|
|
766
|
+
let group = prev.users[key];
|
|
767
|
+
return group ? {
|
|
768
|
+
...prev,
|
|
769
|
+
users: {
|
|
770
|
+
...prev.users,
|
|
771
|
+
[key]: {
|
|
772
|
+
...group,
|
|
773
|
+
lastLoadMoreRequest: timestamp
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
} : prev;
|
|
777
|
+
}, setUsersError = (key, error) => (prev) => {
|
|
778
|
+
let group = prev.users[key];
|
|
779
|
+
return group ? {
|
|
780
|
+
...prev,
|
|
781
|
+
users: {
|
|
782
|
+
...prev.users,
|
|
783
|
+
[key]: {
|
|
784
|
+
...group,
|
|
785
|
+
error
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
} : prev;
|
|
789
|
+
}, cancelRequest = (key) => (prev) => {
|
|
790
|
+
let group = prev.users[key];
|
|
791
|
+
return !group || group.subscriptions.length ? prev : {
|
|
792
|
+
...prev,
|
|
793
|
+
users: omitProperty(prev.users, key)
|
|
794
|
+
};
|
|
795
|
+
}, initializeRequest = (key) => (prev) => prev.users[key] ? prev : {
|
|
796
|
+
...prev,
|
|
797
|
+
users: {
|
|
798
|
+
...prev.users,
|
|
799
|
+
[key]: { subscriptions: [] }
|
|
800
|
+
}
|
|
801
|
+
};
|
|
802
|
+
/**
|
|
803
|
+
* Emits Live Content API messages for a dataset so consumers can invalidate
|
|
804
|
+
* fetched data via sync tags.
|
|
805
|
+
*
|
|
806
|
+
* All consumers observe the same underlying EventSource connection:
|
|
807
|
+
* `@sanity/client` caches the live-events stream per URL and connection
|
|
808
|
+
* options, and every subscription made through this function uses the same
|
|
809
|
+
* API version and request tag.
|
|
810
|
+
*
|
|
811
|
+
* @internal
|
|
812
|
+
*/
|
|
813
|
+
function observeLiveEvents(instance, { resource, onCorsError }) {
|
|
814
|
+
return getClientState(instance, {
|
|
815
|
+
apiVersion: "v2025-05-06",
|
|
816
|
+
resource
|
|
817
|
+
}).observable.pipe(switchMap((client) => defer(() => client.live.events({
|
|
818
|
+
includeDrafts: !!client.config().token,
|
|
819
|
+
tag: "live-events"
|
|
820
|
+
})).pipe(catchError((error) => {
|
|
821
|
+
if (error instanceof CorsOriginError) return onCorsError(error), EMPTY;
|
|
822
|
+
if (error instanceof DisconnectError || error instanceof ConnectionFailedError && typeof error.status == "number" && error.status >= 400 && error.status < 500) return EMPTY;
|
|
823
|
+
throw error;
|
|
824
|
+
}), retry({ delay: 1e3 }))), filter((e) => e.type === "message"));
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* Emits every release document (including archived and published), refetching
|
|
828
|
+
* whenever a Live Content API event matches the query's sync tags.
|
|
829
|
+
*
|
|
830
|
+
* This fetches directly through the client store — deliberately not via the
|
|
831
|
+
* query store — because release data is itself an input to query perspective
|
|
832
|
+
* resolution (`getPerspectiveState`); fetching through the query store would
|
|
833
|
+
* make the two stores mutually dependent.
|
|
834
|
+
*
|
|
835
|
+
* @internal
|
|
836
|
+
*/
|
|
837
|
+
function observeReleases(instance, { resource, onCorsError }) {
|
|
838
|
+
return getClientState(instance, {
|
|
839
|
+
apiVersion: "v2025-05-06",
|
|
840
|
+
resource
|
|
841
|
+
}).observable.pipe(switchMap((client) => {
|
|
842
|
+
let syncTags$ = new BehaviorSubject(void 0);
|
|
843
|
+
return combineLatest([observeLiveEvents(instance, {
|
|
844
|
+
resource,
|
|
845
|
+
onCorsError
|
|
846
|
+
}).pipe(startWith(void 0)), syncTags$]).pipe(filter(([message, syncTags]) => message === void 0 || message.tags.some((tag) => syncTags?.includes(tag))), map(([message]) => message?.id), distinctUntilChanged()).pipe(switchMap((lastLiveEventId) => client.observable.fetch("releases::all()", {}, {
|
|
847
|
+
perspective: "raw",
|
|
848
|
+
filterResponse: !1,
|
|
849
|
+
returnQuery: !1,
|
|
850
|
+
lastLiveEventId,
|
|
851
|
+
tag: "releases"
|
|
852
|
+
})), tap((response) => {
|
|
853
|
+
syncTags$.next(response.syncTags);
|
|
854
|
+
}), map((response) => response.result));
|
|
855
|
+
}), startWith(void 0));
|
|
856
|
+
}
|
|
857
|
+
function sortReleases(releases = []) {
|
|
858
|
+
return [...releases].sort((a, b) => {
|
|
859
|
+
let aType = a.metadata?.releaseType, bType = b.metadata?.releaseType;
|
|
860
|
+
if (aType === "undecided" && bType !== "undecided") return -1;
|
|
861
|
+
if (aType !== "undecided" && bType === "undecided") return 1;
|
|
862
|
+
if (aType === "undecided" && bType === "undecided") return new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime();
|
|
863
|
+
if (aType === "scheduled" && bType === "scheduled") {
|
|
864
|
+
let aPublishAt = a.publishAt || a.metadata?.intendedPublishAt;
|
|
865
|
+
if (!aPublishAt) return 1;
|
|
866
|
+
let bPublishAt = b.publishAt || b.metadata?.intendedPublishAt;
|
|
867
|
+
return bPublishAt ? new Date(bPublishAt).getTime() - new Date(aPublishAt).getTime() : -1;
|
|
868
|
+
}
|
|
869
|
+
return aType === "asap" && bType !== "asap" ? 1 : aType !== "asap" && bType === "asap" ? -1 : aType === "asap" && bType === "asap" ? new Date(b._createdAt).getTime() - new Date(a._createdAt).getTime() : 0;
|
|
870
|
+
});
|
|
871
|
+
}
|
|
872
|
+
const ARCHIVED_RELEASE_STATES = ["archived", "published"], STABLE_EMPTY_RELEASES = [], releasesStore = defineStore({
|
|
873
|
+
name: "Releases",
|
|
874
|
+
getInitialState: () => ({
|
|
875
|
+
activeReleases: void 0,
|
|
876
|
+
allReleases: void 0
|
|
877
|
+
}),
|
|
878
|
+
initialize: (context) => {
|
|
879
|
+
let subscription = subscribeToReleases(context);
|
|
880
|
+
return () => subscription.unsubscribe();
|
|
881
|
+
}
|
|
882
|
+
}), _getActiveReleasesState = bindActionByResource(releasesStore, createStateSourceAction({ selector: ({ state }) => {
|
|
883
|
+
if (state.error) throw state.error;
|
|
884
|
+
return state.activeReleases;
|
|
885
|
+
} })), getActiveReleasesState = (instance, options) => _getActiveReleasesState(instance, options ?? {}), _getAllReleasesState = bindActionByResource(releasesStore, createStateSourceAction({ selector: ({ state }) => {
|
|
886
|
+
if (state.error) throw state.error;
|
|
887
|
+
return state.allReleases;
|
|
888
|
+
} })), getAllReleasesState = (instance, options) => _getAllReleasesState(instance, options ?? {}), subscribeToReleases = ({ instance, state, key: { resource } }) => observeReleases(instance, {
|
|
889
|
+
resource,
|
|
890
|
+
onCorsError: (error) => state.set("setError", { error })
|
|
891
|
+
}).pipe(map((releases) => {
|
|
892
|
+
let sorted = sortReleases(releases ?? STABLE_EMPTY_RELEASES).reverse();
|
|
893
|
+
state.set("setReleases", {
|
|
894
|
+
allReleases: sorted,
|
|
895
|
+
activeReleases: sorted.filter((release) => !ARCHIVED_RELEASE_STATES.includes(release.state))
|
|
896
|
+
});
|
|
897
|
+
})).subscribe({ error: (error) => state.set("setError", { error }) }), optionsCache = /* @__PURE__ */ new Map(), selectInstancePerspective = (context, _) => context.instance.config.perspective, selectActiveReleases = (context) => context.state.activeReleases, selectOptions = (_context, options) => options, memoizedOptionsSelector = createSelector([selectActiveReleases, selectOptions], (activeReleases, options) => {
|
|
898
|
+
if (!options || !activeReleases) return options;
|
|
899
|
+
let releaseIds = activeReleases.map((release) => release._id).join(","), nestedCache = optionsCache.get(releaseIds);
|
|
900
|
+
nestedCache || (nestedCache = /* @__PURE__ */ new Map(), optionsCache.set(releaseIds, nestedCache));
|
|
901
|
+
let optionsKey = JSON.stringify(options), cachedOptions = nestedCache.get(optionsKey);
|
|
902
|
+
return cachedOptions || (cachedOptions = options, nestedCache.set(optionsKey, cachedOptions)), cachedOptions;
|
|
903
|
+
}), _getPerspectiveStateSelector = createStateSourceAction({ selector: createSelector([
|
|
904
|
+
selectInstancePerspective,
|
|
905
|
+
selectActiveReleases,
|
|
906
|
+
memoizedOptionsSelector
|
|
907
|
+
], (instancePerspective, activeReleases, memoizedOptions) => {
|
|
908
|
+
let perspective = memoizedOptions?.perspective ?? instancePerspective ?? "drafts";
|
|
909
|
+
if (!isReleasePerspective(perspective)) return perspective;
|
|
910
|
+
if (!activeReleases || activeReleases.length === 0) return;
|
|
911
|
+
let releaseNames = sortReleases(activeReleases).map((release) => release.name), index = releaseNames.findIndex((name) => name === perspective.releaseName);
|
|
912
|
+
if (index < 0) throw Error(`Release "${perspective.releaseName}" not found in active releases`);
|
|
913
|
+
return ["drafts", ...releaseNames.slice(0, index + 1)].filter((name) => !perspective.excludedPerspectives?.includes(name)).reverse();
|
|
914
|
+
}) }), _boundGetPerspectiveState = bindActionByResource(releasesStore, _getPerspectiveStateSelector), getPerspectiveState = (instance, ...rest) => _boundGetPerspectiveState(instance, ...rest.length ? rest : [{}]), QUERY_STATE_CLEAR_DELAY = 1e3, setQueryError = (key, error) => (prev) => {
|
|
915
|
+
let prevQuery = prev.queries[key];
|
|
916
|
+
return prevQuery ? {
|
|
917
|
+
...prev,
|
|
918
|
+
queries: {
|
|
919
|
+
...prev.queries,
|
|
920
|
+
[key]: {
|
|
921
|
+
...prevQuery,
|
|
922
|
+
error
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
} : prev;
|
|
926
|
+
}, setQueryData = (key, result, syncTags) => (prev) => {
|
|
927
|
+
let prevQuery = prev.queries[key];
|
|
928
|
+
return prevQuery ? {
|
|
929
|
+
...prev,
|
|
930
|
+
queries: {
|
|
931
|
+
...prev.queries,
|
|
932
|
+
[key]: {
|
|
933
|
+
...prevQuery,
|
|
934
|
+
result: result ?? null,
|
|
935
|
+
syncTags
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
} : prev;
|
|
939
|
+
}, setLastLiveEventId = (key, lastLiveEventId) => (prev) => {
|
|
940
|
+
let prevQuery = prev.queries[key];
|
|
941
|
+
return prevQuery ? {
|
|
942
|
+
...prev,
|
|
943
|
+
queries: {
|
|
944
|
+
...prev.queries,
|
|
945
|
+
[key]: {
|
|
946
|
+
...prevQuery,
|
|
947
|
+
lastLiveEventId
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
} : prev;
|
|
951
|
+
}, addSubscriber = (key, subscriptionId) => (prev) => {
|
|
952
|
+
let prevQuery = prev.queries[key], subscribers = [...prevQuery?.subscribers ?? [], subscriptionId];
|
|
953
|
+
return {
|
|
954
|
+
...prev,
|
|
955
|
+
queries: {
|
|
956
|
+
...prev.queries,
|
|
957
|
+
[key]: {
|
|
958
|
+
...prevQuery,
|
|
959
|
+
subscribers
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
};
|
|
963
|
+
}, removeSubscriber = (key, subscriptionId) => (prev) => {
|
|
964
|
+
let prevQuery = prev.queries[key];
|
|
965
|
+
if (!prevQuery) return prev;
|
|
966
|
+
let subscribers = prevQuery.subscribers.filter((id) => id !== subscriptionId);
|
|
967
|
+
return subscribers.length ? {
|
|
968
|
+
...prev,
|
|
969
|
+
queries: {
|
|
970
|
+
...prev.queries,
|
|
971
|
+
[key]: {
|
|
972
|
+
...prevQuery,
|
|
973
|
+
subscribers
|
|
974
|
+
}
|
|
975
|
+
}
|
|
976
|
+
} : {
|
|
977
|
+
...prev,
|
|
978
|
+
queries: omitProperty(prev.queries, key)
|
|
979
|
+
};
|
|
980
|
+
}, EMPTY_ARRAY = [], getQueryKey = (options) => JSON.stringify(options), parseQueryKey = (key) => JSON.parse(key);
|
|
981
|
+
/**
|
|
982
|
+
* Ensures the query key includes an effective perspective so that
|
|
983
|
+
* implicit differences (e.g. different instance.config.perspective)
|
|
984
|
+
* don't collide in the dataset-scoped store.
|
|
985
|
+
*
|
|
986
|
+
* Since perspectives are unique, we can depend on the release stacks
|
|
987
|
+
* to be correct when we retrieve the results.
|
|
988
|
+
*
|
|
989
|
+
*/
|
|
990
|
+
function normalizeOptionsWithPerspective(instance, options) {
|
|
991
|
+
if (options.perspective !== void 0) return options;
|
|
992
|
+
let instancePerspective = instance.config.perspective;
|
|
993
|
+
return {
|
|
994
|
+
...options,
|
|
995
|
+
perspective: instancePerspective === void 0 ? "drafts" : instancePerspective
|
|
996
|
+
};
|
|
997
|
+
}
|
|
998
|
+
const queryStore = defineStore({
|
|
999
|
+
name: "QueryStore",
|
|
1000
|
+
getInitialState: () => ({ queries: {} }),
|
|
1001
|
+
initialize(context) {
|
|
1002
|
+
let subscriptions = [listenForNewSubscribersAndFetch(context), listenToLiveClientAndSetLastLiveEventIds(context)];
|
|
1003
|
+
return () => {
|
|
1004
|
+
for (let subscription of subscriptions) subscription.unsubscribe();
|
|
1005
|
+
};
|
|
1006
|
+
}
|
|
1007
|
+
}), errorHandler = (state) => (error) => state.set("setError", { error }), listenForNewSubscribersAndFetch = ({ state, instance }) => state.observable.pipe(map((s) => new Set(Object.keys(s.queries))), distinctUntilChanged((curr, next) => curr.size === next.size && Array.from(next).every((i) => curr.has(i))), startWith(/* @__PURE__ */ new Set()), pairwise(), mergeMap(([curr, next]) => {
|
|
1008
|
+
let added = Array.from(next).filter((i) => !curr.has(i)), removed = Array.from(curr).filter((i) => !next.has(i));
|
|
1009
|
+
return [...added.map((key) => ({
|
|
1010
|
+
key,
|
|
1011
|
+
added: !0
|
|
1012
|
+
})), ...removed.map((key) => ({
|
|
1013
|
+
key,
|
|
1014
|
+
added: !1
|
|
1015
|
+
}))];
|
|
1016
|
+
}), groupBy((i) => i.key), mergeMap((group$) => group$.pipe(switchMap((e) => {
|
|
1017
|
+
if (!e.added) return EMPTY;
|
|
1018
|
+
let lastLiveEventId$ = state.observable.pipe(map((s) => s.queries[group$.key]?.lastLiveEventId), distinctUntilChanged()), { query, params, projectId, dataset, tag, resource, perspective: perspectiveFromOptions, ...restOptions } = parseQueryKey(group$.key), perspective$ = isReleasePerspective(perspectiveFromOptions) ? getPerspectiveState(instance, {
|
|
1019
|
+
perspective: perspectiveFromOptions,
|
|
1020
|
+
resource
|
|
1021
|
+
}).observable.pipe(filter(Boolean)) : of(perspectiveFromOptions ?? "drafts"), client$ = getClientState(instance, {
|
|
1022
|
+
apiVersion: "v2025-05-06",
|
|
1023
|
+
projectId,
|
|
1024
|
+
dataset,
|
|
1025
|
+
resource
|
|
1026
|
+
}).observable;
|
|
1027
|
+
return combineLatest({
|
|
1028
|
+
lastLiveEventId: lastLiveEventId$,
|
|
1029
|
+
client: client$,
|
|
1030
|
+
perspective: perspective$
|
|
1031
|
+
}).pipe(switchMap(({ lastLiveEventId, client, perspective }) => client.observable.fetch(query, params, {
|
|
1032
|
+
...restOptions,
|
|
1033
|
+
perspective,
|
|
1034
|
+
filterResponse: !1,
|
|
1035
|
+
returnQuery: !1,
|
|
1036
|
+
lastLiveEventId,
|
|
1037
|
+
tag
|
|
1038
|
+
})), tap(({ result, syncTags }) => {
|
|
1039
|
+
state.set("setQueryData", setQueryData(group$.key, result, syncTags));
|
|
1040
|
+
}), catchError((error) => (state.set("setQueryError", setQueryError(group$.key, error)), EMPTY)));
|
|
1041
|
+
})))).subscribe({ error: errorHandler(state) }), listenToLiveClientAndSetLastLiveEventIds = ({ state, instance, key: { resource } }) => {
|
|
1042
|
+
let liveMessages$ = observeLiveEvents(instance, {
|
|
1043
|
+
resource,
|
|
1044
|
+
onCorsError: (error) => state.set("setError", { error })
|
|
1045
|
+
}).pipe(share());
|
|
1046
|
+
return state.observable.pipe(mergeMap((s) => Object.entries(s.queries)), groupBy(([key]) => key), mergeMap((group$) => {
|
|
1047
|
+
let syncTags$ = group$.pipe(map(([, queryState]) => queryState), map((i) => i?.syncTags ?? EMPTY_ARRAY), distinctUntilChanged());
|
|
1048
|
+
return combineLatest([liveMessages$, syncTags$]).pipe(filter(([message, syncTags]) => message.tags.some((tag) => syncTags.includes(tag))), tap(([message]) => {
|
|
1049
|
+
state.set("setLastLiveEventId", setLastLiveEventId(group$.key, message.id));
|
|
1050
|
+
}));
|
|
1051
|
+
})).subscribe({ error: errorHandler(state) });
|
|
1052
|
+
};
|
|
1053
|
+
/** @beta */
|
|
1054
|
+
function getQueryState(...args) {
|
|
1055
|
+
return _getQueryState(...args);
|
|
1056
|
+
}
|
|
1057
|
+
const _getQueryState = bindActionByResource(queryStore, createStateSourceAction({
|
|
1058
|
+
selector: ({ state, instance }, options) => {
|
|
1059
|
+
if (state.error) throw state.error;
|
|
1060
|
+
let key = getQueryKey(normalizeOptionsWithPerspective(instance, options)), queryState = state.queries[key];
|
|
1061
|
+
if (queryState?.error) throw queryState.error;
|
|
1062
|
+
return queryState?.result;
|
|
1063
|
+
},
|
|
1064
|
+
onSubscribe: ({ state, instance }, options) => {
|
|
1065
|
+
let subscriptionId = insecureRandomId(), key = getQueryKey(normalizeOptionsWithPerspective(instance, options));
|
|
1066
|
+
return state.set("addSubscriber", addSubscriber(key, subscriptionId)), () => {
|
|
1067
|
+
setCleanupTimeout(() => state.set("removeSubscriber", removeSubscriber(key, subscriptionId)), QUERY_STATE_CLEAR_DELAY);
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
}));
|
|
1071
|
+
/** @beta */
|
|
1072
|
+
function resolveQuery(...args) {
|
|
1073
|
+
return _resolveQuery(...args);
|
|
1074
|
+
}
|
|
1075
|
+
const _resolveQuery = bindActionByResource(queryStore, ({ state, instance }, { signal, ...options }) => {
|
|
1076
|
+
let normalized = normalizeOptionsWithPerspective(instance, options), { getCurrent } = getQueryState(instance, normalized), key = getQueryKey(normalized), subscriptionId = insecureRandomId();
|
|
1077
|
+
state.set("addSubscriber", addSubscriber(key, subscriptionId));
|
|
1078
|
+
let aborted$ = signal ? new Observable((observer) => {
|
|
1079
|
+
let cleanup = () => {
|
|
1080
|
+
signal.removeEventListener("abort", listener);
|
|
1081
|
+
}, listener = () => {
|
|
1082
|
+
observer.error(new DOMException("The operation was aborted.", "AbortError")), observer.complete(), cleanup();
|
|
1083
|
+
};
|
|
1084
|
+
return signal.addEventListener("abort", listener), cleanup;
|
|
1085
|
+
}).pipe(catchError((error) => {
|
|
1086
|
+
throw error instanceof Error && error.name === "AbortError" && state.set("removeSubscriber", removeSubscriber(key, subscriptionId)), error;
|
|
1087
|
+
})) : NEVER, resolved$ = state.observable.pipe(map(getCurrent), first((i) => i !== void 0)), promise = firstValueFrom(race([resolved$, aborted$])), releaseSubscriber = () => {
|
|
1088
|
+
setCleanupTimeout(() => {
|
|
1089
|
+
state.set("removeSubscriber", removeSubscriber(key, subscriptionId));
|
|
1090
|
+
}, QUERY_STATE_CLEAR_DELAY);
|
|
1091
|
+
};
|
|
1092
|
+
return promise.then(releaseSubscriber, releaseSubscriber), promise;
|
|
1093
|
+
}), TITLE_CANDIDATES = [
|
|
1094
|
+
"title",
|
|
1095
|
+
"name",
|
|
1096
|
+
"label",
|
|
1097
|
+
"heading",
|
|
1098
|
+
"header",
|
|
1099
|
+
"caption"
|
|
1100
|
+
], SUBTITLE_CANDIDATES = [
|
|
1101
|
+
"description",
|
|
1102
|
+
"subtitle",
|
|
1103
|
+
...TITLE_CANDIDATES
|
|
1104
|
+
], PREVIEW_PROJECTION = `{
|
|
1105
|
+
// Get all potential title fields
|
|
1106
|
+
"titleCandidates": {
|
|
1107
|
+
${TITLE_CANDIDATES.map((field) => `"${field}": ${field}`).join(",\n ")}
|
|
1108
|
+
},
|
|
1109
|
+
// Get all potential subtitle fields
|
|
1110
|
+
"subtitleCandidates": {
|
|
1111
|
+
${SUBTITLE_CANDIDATES.map((field) => `"${field}": ${field}`).join(",\n ")}
|
|
1112
|
+
},
|
|
1113
|
+
"media": coalesce(
|
|
1114
|
+
select(
|
|
1115
|
+
defined(asset) => {"type": "image-asset", "_ref": asset._ref},
|
|
1116
|
+
defined(image.asset) => {"type": "image-asset", "_ref": image.asset._ref},
|
|
1117
|
+
defined(mainImage.asset) => {"type": "image-asset", "_ref": mainImage.asset._ref},
|
|
1118
|
+
null
|
|
1119
|
+
)
|
|
1120
|
+
),
|
|
1121
|
+
_type,
|
|
1122
|
+
_id,
|
|
1123
|
+
_updatedAt
|
|
1124
|
+
}`;
|
|
1125
|
+
/**
|
|
1126
|
+
* Checks if the provided value has `_ref` property that is a string and starts with `image-`
|
|
1127
|
+
*/
|
|
1128
|
+
function hasImageRef(value) {
|
|
1129
|
+
return isObject(value) && "_ref" in value && typeof value._ref == "string";
|
|
1130
|
+
}
|
|
1131
|
+
/**
|
|
1132
|
+
* Normalizes a media asset to a preview value.
|
|
1133
|
+
* Adds a url to a media asset reference using `@sanity/image-url`.
|
|
1134
|
+
*
|
|
1135
|
+
* @internal
|
|
1136
|
+
*/
|
|
1137
|
+
function normalizeMedia(media, client) {
|
|
1138
|
+
if (!media || !hasImageRef(media)) return null;
|
|
1139
|
+
let url = createImageUrlBuilder(client).image({ _ref: media._ref }).url();
|
|
1140
|
+
return {
|
|
1141
|
+
type: "image-asset",
|
|
1142
|
+
_ref: media._ref,
|
|
1143
|
+
url
|
|
1144
|
+
};
|
|
1145
|
+
}
|
|
1146
|
+
/**
|
|
1147
|
+
* Finds a single field value from a set of candidates based on a priority list of field names.
|
|
1148
|
+
* Returns the first non-empty string value found from the candidates matching the priority list order.
|
|
1149
|
+
*
|
|
1150
|
+
* @internal
|
|
1151
|
+
*/
|
|
1152
|
+
function findFirstDefined(fieldsToSearch, candidates, exclude) {
|
|
1153
|
+
if (candidates) for (let field of fieldsToSearch) {
|
|
1154
|
+
let value = candidates[field];
|
|
1155
|
+
if (typeof value == "string" && value.trim() !== "" && value !== exclude) return value;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
/**
|
|
1159
|
+
* Transforms a projection result (with titleCandidates, subtitleCandidates, media)
|
|
1160
|
+
* into a PreviewValue (with title, subtitle, media).
|
|
1161
|
+
*
|
|
1162
|
+
* @param projectionResult - The raw projection result from GROQ
|
|
1163
|
+
* @param instance - The Sanity instance to use for client configuration
|
|
1164
|
+
* @param resource - Data resource for the preview
|
|
1165
|
+
* @internal
|
|
1166
|
+
*/
|
|
1167
|
+
function transformProjectionToPreview(instance, projectionResult, resource) {
|
|
1168
|
+
let title = findFirstDefined(TITLE_CANDIDATES, projectionResult.titleCandidates), subtitle = findFirstDefined(SUBTITLE_CANDIDATES, projectionResult.subtitleCandidates, title), client = getClient(instance, {
|
|
1169
|
+
apiVersion: "v2025-05-06",
|
|
1170
|
+
resource
|
|
1171
|
+
});
|
|
1172
|
+
return {
|
|
1173
|
+
title: String(title || `${projectionResult._type}: ${projectionResult._id}`),
|
|
1174
|
+
subtitle: subtitle || void 0,
|
|
1175
|
+
media: normalizeMedia(projectionResult.media, client),
|
|
1176
|
+
...projectionResult._status && { _status: projectionResult._status }
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
const TOKEN_REGEX = /(?:[^\s"]+|"[^"]*")+/g;
|
|
1180
|
+
/**
|
|
1181
|
+
* @internal
|
|
1182
|
+
* Checks if a token starts with the negation character.
|
|
1183
|
+
*/
|
|
1184
|
+
function isNegationToken(token) {
|
|
1185
|
+
return token !== void 0 && token.trim().startsWith("-");
|
|
1186
|
+
}
|
|
1187
|
+
/**
|
|
1188
|
+
* @internal
|
|
1189
|
+
* Checks if a token ends with the wildcard character.
|
|
1190
|
+
*/
|
|
1191
|
+
function isPrefixToken(token) {
|
|
1192
|
+
return token !== void 0 && token.trim().endsWith("*");
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* @internal
|
|
1196
|
+
* Checks if a token is enclosed in double quotes.
|
|
1197
|
+
*/
|
|
1198
|
+
function isExactMatchToken(token) {
|
|
1199
|
+
return !!token && token.length >= 2 && token.startsWith("\"") && token.endsWith("\"");
|
|
1200
|
+
}
|
|
1201
|
+
/**
|
|
1202
|
+
* Creates a GROQ search filter string (`[@] match text::query("...")`)
|
|
1203
|
+
* from a raw search query string.
|
|
1204
|
+
*
|
|
1205
|
+
* It applies wildcard ('*') logic to the last eligible token and escapes
|
|
1206
|
+
* double quotes within the search term.
|
|
1207
|
+
*
|
|
1208
|
+
* If the input query is empty or only whitespace, it returns an empty string.
|
|
1209
|
+
*
|
|
1210
|
+
* @param query - The raw input search string.
|
|
1211
|
+
* @returns The GROQ search filter string, or an empty string.
|
|
1212
|
+
* @internal
|
|
1213
|
+
*/
|
|
1214
|
+
function createGroqSearchFilter(query) {
|
|
1215
|
+
let trimmedQuery = query.trim();
|
|
1216
|
+
if (!trimmedQuery) return "";
|
|
1217
|
+
let tokens = trimmedQuery.match(TOKEN_REGEX) ?? [], reversedIndex = [...tokens].reverse().findIndex((token) => !isNegationToken(token) && !isExactMatchToken(token)), finalIncrementalTokenIndex = reversedIndex === -1 ? -1 : tokens.length - 1 - reversedIndex, finalIncrementalToken = tokens[finalIncrementalTokenIndex], processedTokens = [...tokens];
|
|
1218
|
+
return finalIncrementalToken !== void 0 && !isPrefixToken(finalIncrementalToken) && processedTokens.splice(finalIncrementalTokenIndex, 1, `${finalIncrementalToken}*`), `[@] match text::query("${processedTokens.join(" ").replace(/"/g, "\\\"")}")`;
|
|
1219
|
+
}
|
|
1220
|
+
export { addComment as A, normalizeComment as B, getCommentThreadsState as C, resolveCommentThreads as D, parseCommentsOptionsKey as E, removeCommentById as F, COMMENTS_API_VERSION as G, assertDatasetResource as H, restoreComments as I, setCleanupTimeout as K, rollbackCommentUpdate as L, clearPendingTransaction as M, getCommentsKey as N, resolveComments as O, receiveComment as P, setCommentCreateError as R, commentsStore as S, getCommentsState as T, getAddonDatasetState as U, toCommentFieldPath as V, provisionAddonDataset as W, setUsersData as _, getQueryState as a, PROJECT_API_VERSION as b, getPerspectiveState as c, addSubscription as d, cancelRequest as f, removeSubscription as g, parseUsersKey as h, getQueryKey as i, applyCommentUpdate as j, toCommentsKeyParts as k, getActiveReleasesState as l, initializeRequest as m, transformProjectionToPreview as n, parseQueryKey as o, getUsersKey as p, insecureRandomId as q, PREVIEW_PROJECTION as r, resolveQuery as s, createGroqSearchFilter as t, getAllReleasesState as u, setUsersError as v, getCommentsOptionsKey as w, USERS_STATE_CLEAR_DELAY as x, updateLastLoadMoreRequest as y, setPendingTransaction as z };
|
|
1221
|
+
|
|
1222
|
+
//# sourceMappingURL=createGroqSearchFilter-B0NdA-EP.js.map
|