@sanity/orderable-document-list 1.3.5 → 1.4.1

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.
@@ -16,6 +16,8 @@ export interface DocumentListWrapperProps {
16
16
  filter?: string
17
17
  // eslint-disable-next-line react/require-default-props
18
18
  params?: Record<string, unknown>
19
+ // eslint-disable-next-line react/require-default-props
20
+ currentVersion?: string
19
21
  }
20
22
 
21
23
  // 1. Validate first that the schema has been configured for ordering
@@ -26,6 +28,7 @@ export function DocumentListWrapper({
26
28
  resetOrderTransaction,
27
29
  filter,
28
30
  params,
31
+ currentVersion,
29
32
  }: DocumentListWrapperProps) {
30
33
  const toast = useToast()
31
34
  const schema = useSchema()
@@ -98,7 +101,12 @@ export function DocumentListWrapper({
98
101
 
99
102
  return (
100
103
  <OrderableContext.Provider value={{showIncrements}}>
101
- <DocumentListQuery type={type} filter={filter} params={params} />
104
+ <DocumentListQuery
105
+ type={type}
106
+ filter={filter}
107
+ params={params}
108
+ currentVersion={currentVersion}
109
+ />
102
110
  </OrderableContext.Provider>
103
111
  )
104
112
  }
@@ -11,6 +11,7 @@ export interface OrderableDocumentListProps {
11
11
  client: SanityClient
12
12
  filter?: string
13
13
  params?: Record<string, unknown>
14
+ currentVersion?: string
14
15
  }
15
16
  }
16
17
 
@@ -73,6 +74,7 @@ export class OrderableDocumentList extends Component<OrderableDocumentListProps,
73
74
  type={type}
74
75
  showIncrements={this.state.showIncrements}
75
76
  resetOrderTransaction={this.state.resetOrderTransaction}
77
+ currentVersion={this?.props?.options?.currentVersion}
76
78
  />
77
79
  )
78
80
  }
@@ -30,7 +30,11 @@ export function orderableDocumentListDeskItem(config: OrderableListConfig): List
30
30
 
31
31
  const {type, filter, menuItems = [], createIntent, params, title, icon, id, context, S} = config
32
32
  const {schema, getClient} = context
33
+ // 'perspectiveStack' may not exist on ConfigContext in some versions
34
+ const perspectiveStack = (context as any).perspectiveStack || []
33
35
  const client = getClient({apiVersion: API_VERSION})
36
+ // the first position in the perspective stack is the current version
37
+ const currentVersion = perspectiveStack[0]
34
38
 
35
39
  const listTitle = title ?? `Orderable ${type}`
36
40
  const listId = id ?? `orderable-${type}`
@@ -63,7 +67,7 @@ export function orderableDocumentListDeskItem(config: OrderableListConfig): List
63
67
 
64
68
  type: 'component',
65
69
  component: OrderableDocumentList,
66
- options: {type, filter, params, client},
70
+ options: {type, filter, params, client, currentVersion},
67
71
  menuItems: [
68
72
  ...menuItems,
69
73
  S.menuItem().title(`Reset Order`).icon(GenerateIcon).action(`resetOrder`).serialize(),
@@ -0,0 +1,327 @@
1
+ import {describe, it, expect} from 'vitest'
2
+ import {type SanityDocumentWithOrder} from '../../types'
3
+ import {getFilteredDedupedDocs} from '../getFilteredDedupedDocs'
4
+
5
+ describe('getFilteredDedupedDocs', () => {
6
+ const mockDocumentsFromVersionPerspective: SanityDocumentWithOrder[] = [
7
+ {
8
+ _id: 'drafts.document-1',
9
+ _type: 'orderableCategory',
10
+ orderRank: '0|10000o:',
11
+ _createdAt: new Date().toISOString(),
12
+ _updatedAt: new Date().toISOString(),
13
+ _rev: '123',
14
+ },
15
+ {
16
+ _id: 'versions.rmWGG9z1W.document-2',
17
+ _type: 'orderableCategory',
18
+ orderRank: '0|100014:',
19
+ _createdAt: new Date().toISOString(),
20
+ _updatedAt: new Date().toISOString(),
21
+ _rev: '123',
22
+ },
23
+ {
24
+ _id: 'drafts.document-3',
25
+ _type: 'orderableCategory',
26
+ orderRank: '0|10001k:',
27
+ _createdAt: new Date().toISOString(),
28
+ _updatedAt: new Date().toISOString(),
29
+ _rev: '123',
30
+ },
31
+ {
32
+ _id: 'document-4',
33
+ _type: 'orderableCategory',
34
+ orderRank: '0|100020:',
35
+ _createdAt: new Date().toISOString(),
36
+ _updatedAt: new Date().toISOString(),
37
+ _rev: '123',
38
+ },
39
+ {
40
+ _id: 'versions.rmWGG9z1W.document-6',
41
+ _type: 'orderableCategory',
42
+ orderRank: '0|10002w:',
43
+ _createdAt: new Date().toISOString(),
44
+ _updatedAt: new Date().toISOString(),
45
+ _rev: '123',
46
+ },
47
+ {
48
+ _id: 'document-5',
49
+ _type: 'orderableCategory',
50
+ orderRank: '0|100034:',
51
+ _createdAt: new Date().toISOString(),
52
+ _updatedAt: new Date().toISOString(),
53
+ _rev: '123',
54
+ },
55
+ {
56
+ _id: 'drafts.document-5',
57
+ _type: 'orderableCategory',
58
+ orderRank: '0|100034:',
59
+ _createdAt: new Date().toISOString(),
60
+ _updatedAt: new Date().toISOString(),
61
+ _rev: '123',
62
+ },
63
+ {
64
+ _id: 'document-9',
65
+ _type: 'orderableCategory',
66
+ orderRank: '0|10003c:',
67
+ _createdAt: new Date().toISOString(),
68
+ _updatedAt: new Date().toISOString(),
69
+ _rev: '123',
70
+ },
71
+ {
72
+ _id: 'drafts.document-8',
73
+ _type: 'orderableCategory',
74
+ orderRank: '0|10003s:',
75
+ _createdAt: new Date().toISOString(),
76
+ _updatedAt: new Date().toISOString(),
77
+ _rev: '123',
78
+ },
79
+ {
80
+ _id: 'versions.rmWGG9z1W.document-3',
81
+ _type: 'orderableCategory',
82
+ orderRank: '0|100048:',
83
+ _createdAt: new Date().toISOString(),
84
+ _updatedAt: new Date().toISOString(),
85
+ _rev: '123',
86
+ },
87
+ ]
88
+
89
+ const mockDocumentsInDraftsPerspective: SanityDocumentWithOrder[] = [
90
+ {
91
+ _id: 'drafts.document-1',
92
+ _type: 'orderableCategory',
93
+ orderRank: '0|10000o:',
94
+ _createdAt: new Date().toISOString(),
95
+ _updatedAt: new Date().toISOString(),
96
+ _rev: '123',
97
+ },
98
+ {
99
+ _id: 'drafts.document-3',
100
+ _type: 'orderableCategory',
101
+ orderRank: '0|10001k:',
102
+ _createdAt: new Date().toISOString(),
103
+ _updatedAt: new Date().toISOString(),
104
+ _rev: '123',
105
+ },
106
+ {
107
+ _id: 'document-4',
108
+ _type: 'orderableCategory',
109
+ orderRank: '0|100020:',
110
+ _createdAt: new Date().toISOString(),
111
+ _updatedAt: new Date().toISOString(),
112
+ _rev: '123',
113
+ },
114
+ {
115
+ _id: 'document-5',
116
+ _type: 'orderableCategory',
117
+ orderRank: '0|100034:',
118
+ _createdAt: new Date().toISOString(),
119
+ _updatedAt: new Date().toISOString(),
120
+ _rev: '123',
121
+ },
122
+ {
123
+ _id: 'drafts.document-5',
124
+ _type: 'orderableCategory',
125
+ orderRank: '0|100034:',
126
+ _createdAt: new Date().toISOString(),
127
+ _updatedAt: new Date().toISOString(),
128
+ _rev: '123',
129
+ },
130
+ {
131
+ _id: 'document-7',
132
+ _type: 'orderableCategory',
133
+ orderRank: '0|10003c:',
134
+ _createdAt: new Date().toISOString(),
135
+ _updatedAt: new Date().toISOString(),
136
+ _rev: '123',
137
+ },
138
+ {
139
+ _id: 'drafts.document-8',
140
+ _type: 'orderableCategory',
141
+ orderRank: '0|10003s:',
142
+ _createdAt: new Date().toISOString(),
143
+ _updatedAt: new Date().toISOString(),
144
+ _rev: '123',
145
+ },
146
+ ]
147
+
148
+ const mockDocumentsPublishedPerspective: SanityDocumentWithOrder[] = [
149
+ {
150
+ _id: '123',
151
+ _type: 'orderableCategory',
152
+ orderRank: '0|100035:',
153
+ _createdAt: new Date().toISOString(),
154
+ _updatedAt: new Date().toISOString(),
155
+ _rev: '123',
156
+ },
157
+ {
158
+ _id: '456',
159
+ _type: 'orderableCategory',
160
+ orderRank: '0|100036:',
161
+ _createdAt: new Date().toISOString(),
162
+ _updatedAt: new Date().toISOString(),
163
+ _rev: '123',
164
+ },
165
+ {
166
+ _id: '789',
167
+ _type: 'orderableCategory',
168
+ orderRank: '0|10003h:',
169
+ _createdAt: new Date().toISOString(),
170
+ _updatedAt: new Date().toISOString(),
171
+ _rev: '123',
172
+ },
173
+ ]
174
+
175
+ it('should filter and deduplicate documents in draft perspective', () => {
176
+ const result = getFilteredDedupedDocs(
177
+ mockDocumentsInDraftsPerspective as SanityDocumentWithOrder[],
178
+ 'drafts',
179
+ )
180
+
181
+ // Should only include drafts and published (if it doesn't have a published)
182
+ expect(result).toHaveLength(6)
183
+ expect(result.map((doc) => doc._id)).toEqual([
184
+ 'drafts.document-1',
185
+ 'drafts.document-3',
186
+ 'document-4',
187
+ 'drafts.document-5',
188
+ 'document-7',
189
+ 'drafts.document-8',
190
+ ])
191
+ })
192
+
193
+ it('should prioritize drafts over published in draft perspective', () => {
194
+ const documentsWithDuplicates = [
195
+ {
196
+ _id: 'drafts.123',
197
+ _type: 'test',
198
+ orderRank: 'a',
199
+ _createdAt: new Date().toISOString(),
200
+ _updatedAt: new Date().toISOString(),
201
+ _rev: '123',
202
+ },
203
+ {
204
+ _id: '123',
205
+ _type: 'test',
206
+ orderRank: 'b',
207
+ _createdAt: new Date().toISOString(),
208
+ _updatedAt: new Date().toISOString(),
209
+ _rev: '123',
210
+ },
211
+ ]
212
+
213
+ const result = getFilteredDedupedDocs(documentsWithDuplicates, 'drafts')
214
+
215
+ // Should only show the draft version
216
+ expect(result).toHaveLength(1)
217
+ expect(result[0]._id).toBe('drafts.123')
218
+ })
219
+
220
+ it('should filter and deduplicate documents in published perspective', () => {
221
+ const result = getFilteredDedupedDocs(mockDocumentsPublishedPerspective, 'published')
222
+
223
+ // Should only include published documents
224
+ expect(result).toHaveLength(3)
225
+ expect(result.map((doc) => doc._id)).toEqual(['123', '456', '789'])
226
+ })
227
+
228
+ it('should filter and deduplicate documents in version perspective', () => {
229
+ const result = getFilteredDedupedDocs(
230
+ mockDocumentsFromVersionPerspective as SanityDocumentWithOrder[],
231
+ 'rmWGG9z1W',
232
+ )
233
+
234
+ // Should include versions for the specific release, drafts, and published
235
+ expect(result).toHaveLength(8)
236
+ expect(result.map((doc) => doc._id)).toEqual([
237
+ 'drafts.document-1',
238
+ 'versions.rmWGG9z1W.document-2',
239
+ 'document-4',
240
+ 'versions.rmWGG9z1W.document-6',
241
+ 'drafts.document-5',
242
+ 'document-9',
243
+ 'drafts.document-8',
244
+ 'versions.rmWGG9z1W.document-3',
245
+ ])
246
+ })
247
+
248
+ it('should prioritize versions over drafts in version perspective', () => {
249
+ const documentsWithVersionAndDraft: SanityDocumentWithOrder[] = [
250
+ {
251
+ _id: 'drafts.document-1',
252
+ _type: 'orderableCategory',
253
+ orderRank: '0|10001k:',
254
+ _createdAt: new Date().toISOString(),
255
+ _updatedAt: new Date().toISOString(),
256
+ _rev: '123',
257
+ },
258
+ {
259
+ _id: 'versions.rEZnogJnx.document-1',
260
+ _type: 'orderableCategory',
261
+ orderRank: '0|10000o:',
262
+ _createdAt: new Date().toISOString(),
263
+ _updatedAt: new Date().toISOString(),
264
+ _rev: '123',
265
+ },
266
+ ]
267
+
268
+ const result = getFilteredDedupedDocs(documentsWithVersionAndDraft, 'rEZnogJnx')
269
+
270
+ // Should prioritize the version over the draft
271
+ expect(result).toHaveLength(1)
272
+ expect(result.map((doc) => doc._id)).toEqual(['versions.rEZnogJnx.document-1'])
273
+ })
274
+
275
+ it('should show draft of other versions when the selected version doesnt exist for the document', () => {
276
+ const result = getFilteredDedupedDocs(
277
+ [
278
+ {
279
+ _id: 'drafts.123',
280
+ _type: 'test',
281
+ orderRank: 'a',
282
+ _createdAt: new Date().toISOString(),
283
+ _updatedAt: new Date().toISOString(),
284
+ _rev: '123',
285
+ },
286
+ {
287
+ _id: 'versions.rEZnogJnx.123',
288
+ _type: 'test',
289
+ orderRank: 'b',
290
+ _createdAt: new Date().toISOString(),
291
+ _updatedAt: new Date().toISOString(),
292
+ _rev: '123',
293
+ },
294
+ {
295
+ _id: 'versions.other-version.456',
296
+ _type: 'test',
297
+ orderRank: 'b',
298
+ _createdAt: new Date().toISOString(),
299
+ _updatedAt: new Date().toISOString(),
300
+ _rev: '123',
301
+ },
302
+ {
303
+ _id: '789',
304
+ _type: 'test',
305
+ orderRank: 'b',
306
+ _createdAt: new Date().toISOString(),
307
+ _updatedAt: new Date().toISOString(),
308
+ _rev: '123',
309
+ },
310
+ ],
311
+ 'other-version',
312
+ )
313
+
314
+ // Should show the draft of the other version
315
+ expect(result).toHaveLength(3)
316
+ expect(result.map((doc) => doc._id)).toEqual([
317
+ 'drafts.123',
318
+ 'versions.other-version.456',
319
+ '789',
320
+ ])
321
+ })
322
+
323
+ it('should handle empty input', () => {
324
+ const result = getFilteredDedupedDocs([], 'drafts')
325
+ expect(result).toEqual([])
326
+ })
327
+ })
@@ -1,6 +1,8 @@
1
- import {type SanityClient, useClient} from 'sanity'
1
+ import {type SanityClient, useClient, usePerspective} from 'sanity'
2
2
  import {API_VERSION} from './constants'
3
3
 
4
4
  export function useSanityClient(): SanityClient {
5
- return useClient({apiVersion: API_VERSION})
5
+ const {perspectiveStack} = usePerspective()
6
+
7
+ return useClient({apiVersion: API_VERSION}).withConfig({perspective: perspectiveStack})
6
8
  }
@@ -1,3 +1,4 @@
1
1
  export const ORDER_FIELD_NAME = `orderRank` as const
2
2
 
3
- export const API_VERSION = `2021-09-01` as const
3
+ // same version we are using in the vision plugin
4
+ export const API_VERSION = `v2025-06-27` as const
@@ -0,0 +1,103 @@
1
+ import {getPublishedId, getVersionFromId, isDraftId, isPublishedId, isVersionId} from 'sanity'
2
+ import {type SanityDocumentWithOrder} from '../types'
3
+
4
+ const isVersionForCurrentPerspective = (
5
+ document: SanityDocumentWithOrder,
6
+ perspectiveName: string,
7
+ publishedId: string,
8
+ ) => {
9
+ return (
10
+ document._id &&
11
+ isVersionId(document._id) &&
12
+ getVersionFromId(document._id) === perspectiveName &&
13
+ getPublishedId(document._id) === publishedId
14
+ )
15
+ }
16
+
17
+ // this removes dedupped docs from the list and makes sure that it keeps the right docs
18
+ // in the list while preserving the order established by the orderRank field
19
+ export const getFilteredDedupedDocs = (
20
+ documents: SanityDocumentWithOrder[],
21
+ perspectiveName?: string,
22
+ ): SanityDocumentWithOrder[] => {
23
+ // Flatten the documents array in case it's nested (like in test data)
24
+ const flatDocuments = documents.flat()
25
+
26
+ return flatDocuments.reduce<SanityDocumentWithOrder[]>((acc, cur) => {
27
+ if (!cur._id) {
28
+ return acc
29
+ }
30
+
31
+ // Handle version-only documents
32
+ if (isVersionId(cur._id)) {
33
+ const versionFromId = getVersionFromId(cur._id)
34
+ const isCorrectVersion = versionFromId === perspectiveName
35
+
36
+ // Only include versions that match the current perspective
37
+ if (
38
+ perspectiveName &&
39
+ perspectiveName !== 'drafts' &&
40
+ perspectiveName !== 'published' &&
41
+ isCorrectVersion
42
+ ) {
43
+ return [...acc, cur]
44
+ }
45
+ return acc
46
+ }
47
+
48
+ // Handle published perspective - only include published documents
49
+ if (perspectiveName === 'published') {
50
+ if (isPublishedId(cur._id)) {
51
+ return [...acc, cur]
52
+ }
53
+ return acc
54
+ }
55
+
56
+ // in situations where the document is not a draft, we need to check if
57
+ // the version should override a published document or a draft
58
+ if (!isDraftId(cur._id)) {
59
+ const publishedId = getPublishedId(cur._id)
60
+
61
+ const countNrPublished = JSON.stringify(flatDocuments).match(`/${publishedId}/g`)
62
+
63
+ // Check if there's a version that matches the perspectiveName
64
+ const hasMatchingVersion =
65
+ perspectiveName && perspectiveName !== 'drafts' && perspectiveName !== 'published'
66
+ ? flatDocuments.some((doc) =>
67
+ isVersionForCurrentPerspective(doc, perspectiveName, publishedId),
68
+ )
69
+ : false
70
+
71
+ // Check if there's a draft
72
+ const hasDraft = flatDocuments.some((doc) => doc._id === `drafts.${cur._id}`)
73
+
74
+ // Priority: version > draft > published
75
+ // If there's a matching version, skip published
76
+ if (hasMatchingVersion) {
77
+ return acc
78
+ }
79
+
80
+ // eslint-disable-next-line max-nested-callbacks
81
+ const alsoHasDraft = hasDraft || countNrPublished
82
+ return alsoHasDraft ? acc : [...acc, cur]
83
+ }
84
+
85
+ // For drafts, check if there's a version for this document in version perspective
86
+ if (perspectiveName && perspectiveName !== 'drafts' && perspectiveName !== 'published') {
87
+ const baseId = getPublishedId(cur._id)
88
+ const hasVersion = flatDocuments.some((doc) =>
89
+ isVersionForCurrentPerspective(doc, perspectiveName, baseId),
90
+ )
91
+
92
+ // If there's a version for this document, skip the draft
93
+ if (hasVersion) {
94
+ return acc
95
+ }
96
+ }
97
+
98
+ // Check if the draft has a published version
99
+ cur.hasPublished = flatDocuments.some((doc) => doc._id === cur._id.replace(`drafts.`, ``))
100
+
101
+ return [...acc, cur]
102
+ }, [])
103
+ }
@@ -4,6 +4,7 @@ export interface DocumentListQueryProps {
4
4
  type: string
5
5
  filter?: string
6
6
  params?: Record<string, unknown>
7
+ currentVersion?: string
7
8
  }
8
9
 
9
10
  export interface DocumentListQueryResult {
@@ -17,8 +18,23 @@ export function getDocumentQuery({
17
18
  type,
18
19
  filter,
19
20
  params = DEFAULT_PARAMS,
21
+ currentVersion,
20
22
  }: DocumentListQueryProps): DocumentListQueryResult {
21
- const querySelect = `*[_type == $type ${filter ? `&& ${filter}` : ''}]`
23
+ let perspectiveFilter = null
24
+ if (currentVersion === 'published') {
25
+ perspectiveFilter = '!(_id in path("drafts.**")) && !(_id in path("versions.**"))'
26
+ } else if (currentVersion === 'drafts') {
27
+ // Show drafts, and published when no draft exists
28
+ perspectiveFilter = `
29
+ (_id in path("drafts.**") || (!(_id in path("drafts.**")) && !(_id in path("versions.**"))))
30
+ `
31
+ } else {
32
+ // Default behavior: prioritize drafts over published when both exist
33
+ // the priority should be a version
34
+ perspectiveFilter = `(sanity::partOfRelease($currentVersion) || (!(_id in path("drafts.**")) && !(_id in path("versions.**"))) || (_id in path("drafts.**")))`
35
+ }
36
+
37
+ const querySelect = `*[_type == $type ${perspectiveFilter ? `&& ${perspectiveFilter}` : ''}${filter ? `&& ${filter}` : ''}]`
22
38
  const queryOrder = `|order(@[$order] asc)`
23
39
  const queryFields = `{_id, _type, ${ORDER_FIELD_NAME}}`
24
40
 
@@ -27,6 +43,7 @@ export function getDocumentQuery({
27
43
  ...params,
28
44
  type,
29
45
  order: ORDER_FIELD_NAME,
46
+ ...(currentVersion && {currentVersion}),
30
47
  }
31
48
 
32
49
  return {query, queryParams}
@@ -5,13 +5,15 @@ import {DocumentListQueryProps, getDocumentQuery} from './query'
5
5
 
6
6
  export interface ResetOrderParams extends DocumentListQueryProps {
7
7
  client: SanityClient
8
+ currentVersion?: string
8
9
  }
9
10
 
10
11
  // Function to wipe and re-do ordering with LexoRank
11
12
  // Will at least attempt to start with the current order
12
13
  export async function resetOrder(params: ResetOrderParams): Promise<MultipleMutationResult | null> {
13
- const {client, ...queryProps} = params
14
- const {query, queryParams} = getDocumentQuery(queryProps)
14
+ const {client, currentVersion, ...queryProps} = params
15
+ const {query, queryParams} = getDocumentQuery({...queryProps, currentVersion})
16
+
15
17
  const documents = await client.fetch<
16
18
  Array<{
17
19
  _id: string