Skip to content

fix(structure): support array index access in custom sort orderings#12591

Draft
jordanl17 wants to merge 1 commit intomainfrom
fix/structure-array-ordering-projection
Draft

fix(structure): support array index access in custom sort orderings#12591
jordanl17 wants to merge 1 commit intomainfrom
fix/structure-array-ordering-projection

Conversation

@jordanl17
Copy link
Copy Markdown
Member

Description

Custom sort orderings using array index access (e.g., items[0].value) would crash the document list with "nonexistent field" because getExtendedProjection split field paths by . only, which can't parse bracket notation. This affected Unilever, Swiss Post, and Silversea Cruises.

Replaced .split('.') with PathUtils.fromString() which properly tokenizes array indices and keyed access. Added array member type resolution to the projection tree builder so it generates correct GROQ like items[0]->{value}.

Resolves SAPP-3692

What to review

  • getExtendedProjection.ts - the core fix. Two new helpers: reportError (dedups the warn/throw pattern) and recurseIntoField (shared leaf/reference/object recursion).
  • Test coverage for array index, keyed segment, multi-type array, and non-array access errors.
  • withObjectFieldsOrder test-studio schema gains relatedAuthors and tags array fields with array-based orderings for manual verification.

Testing

  • 19 unit tests pass (8 new, 11 existing unchanged)
  • Red-green verified: stashing the impl makes all 8 new tests fail
  • Manual test in dev studio: "With object fields order" sort menu works with array orderings

Notes for release

Custom sort orderings now support array index access in field paths (e.g., items[0].value). Previously, defining an ordering like by: [{field: 'items[0].value', direction: 'asc'}] would crash the document list. Array index and _key-based access are both supported for single-member-type arrays.

@vercel
Copy link
Copy Markdown

vercel bot commented Apr 7, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
page-building-studio Ready Ready Preview, Comment Apr 7, 2026 4:40pm
test-studio Ready Ready Preview, Comment Apr 7, 2026 4:40pm

Request Review

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

📦 Bundle Stats — sanity

Compared against main (29e85014) · v5.20.0 (npm)

sanity

Metric Value vs main (29e8501) vs v5.20.0
Internal (raw) 4.41 MB - -
Internal (gzip) 1.01 MB - -
Bundled (raw) 12.09 MB - -9.5 KB, -0.1%
Bundled (gzip) 2.72 MB - -2.1 KB, -0.1%
Import time 1.53s -5ms, -0.3% +56ms, +3.8%

bin:sanity

Metric Value vs main (29e8501) vs v5.20.0
Internal (raw) 7.1 KB - -
Internal (gzip) 2.9 KB - -
Bundled (raw) 7.1 KB - -
Bundled (gzip) 2.8 KB - -
Import time 5ms -0ms, -0.3% +0ms, +7.2%

🗺️ View treemap · Artifacts

Details
  • Import time regressions over 10% are flagged with ⚠️
  • Sizes shown as raw / gzip 🗜️. Internal bytes = own code only. Total bytes = with all dependencies. Import time = Node.js cold-start median.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

🧪 E2E Preview environment

🔑 Environment Variables for Local Testing

This is the preview URL for the E2E tests: https://e2e-studio-bs1cm25rm.sanity.dev

To run the E2E tests locally, you can use the following environment variables, then run pnpm test:e2e --ui to open the Playwright test runner.

💬 Remember to build the project first with pnpm build:e2e.

  SANITY_E2E_PROJECT_ID=ittbm412
  SANITY_E2E_BASE_URL=https://e2e-studio-bs1cm25rm.sanity.dev
  SANITY_E2E_DATASET="update depending the project you want to test (pr-12591-chromium-24093032455 || pr-12591-firefox-24093032455 )"
  SANITY_E2E_DATASET_CHROMIUM=pr-12591-chromium-24093032455
  SANITY_E2E_DATASET_FIREFOX=pr-12591-firefox-24093032455

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

📊 Playwright Test Report

Download Full E2E Report

This report contains test results, including videos of failing tests.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

📚 TypeDoc Generation Result

TypeDoc generated successfully!

  • File size: 8.2M
  • Total exports: 1063
  • Artifact: sanity-typedoc-e79a07b7780ab24054735441d0ac74c54689b215

The TypeDoc JSON file has been generated and validated. All documentation scripts completed successfully.

@jordanl17 jordanl17 requested a review from Copilot April 7, 2026 16:43
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

⚡️ Editor Performance Report

Updated Tue, 07 Apr 2026 16:50:30 GMT

Benchmark reference
latency of sanity@latest
experiment
latency of this branch
Δ (%)
latency difference
arrayI18n (simple-en) 64.5 efps (16ms) 71.4 efps (14ms) -2ms (-/-%)
article (title) 33.3 efps (30ms) 32.3 efps (31ms) +1ms (+3.3%)
article (body) 50.5 efps (20ms) 50.8 efps (20ms) -0ms (-0.5%)
article (string inside object) 52.6 efps (19ms) 47.6 efps (21ms) +2ms (+10.5%)
article (string inside array) 48.8 efps (21ms) 47.6 efps (21ms) +1ms (+2.4%)
recipe (name) 95.2 efps (11ms) 99.9+ efps (10ms) -1ms (-/-%)
recipe (description) 52.6 efps (19ms) 47.6 efps (21ms) +2ms (+10.5%)
recipe (instructions) 99.9+ efps (8ms) 99.9+ efps (7ms) -2ms (-/-%)
singleString (stringField) 99.9+ efps (6ms) 99.9+ efps (7ms) +1ms (-/-%)
synthetic (title) 52.6 efps (19ms) 55.6 efps (18ms) -1ms (-5.3%)
synthetic (string inside object) 52.6 efps (19ms) 54.1 efps (19ms) -1ms (-2.6%)

efps — editor "frames per second". The number of updates assumed to be possible within a second.

Derived from input latency. efps = 1000 / input_latency

Detailed information

🏠 Reference result

The performance result of sanity@latest

Benchmark latency p75 p90 p99 blocking time test duration
arrayI18n (simple-en) 16ms 18ms 25ms 46ms 0ms 5.8s
article (title) 30ms 35ms 53ms 84ms 39ms 7.9s
article (body) 20ms 34ms 71ms 88ms 227ms 5.5s
article (string inside object) 19ms 21ms 27ms 59ms 0ms 6.1s
article (string inside array) 21ms 23ms 29ms 64ms 1ms 6.7s
recipe (name) 11ms 13ms 15ms 35ms 0ms 5.3s
recipe (description) 19ms 22ms 23ms 33ms 0ms 4.4s
recipe (instructions) 8ms 10ms 12ms 40ms 0ms 3.2s
singleString (stringField) 6ms 9ms 10ms 22ms 0ms 4.5s
synthetic (title) 19ms 22ms 64ms 104ms 1466ms 9.8s
synthetic (string inside object) 19ms 23ms 79ms 92ms 1217ms 9.4s

🧪 Experiment result

The performance result of this branch

Benchmark latency p75 p90 p99 blocking time test duration
arrayI18n (simple-en) 14ms 20ms 26ms 47ms 11ms 5.8s
article (title) 31ms 36ms 63ms 111ms 63ms 8.2s
article (body) 20ms 23ms 30ms 92ms 215ms 5.3s
article (string inside object) 21ms 28ms 37ms 99ms 28ms 6.9s
article (string inside array) 21ms 27ms 41ms 74ms 22ms 7.0s
recipe (name) 10ms 13ms 17ms 36ms 0ms 5.3s
recipe (description) 21ms 23ms 26ms 31ms 0ms 4.6s
recipe (instructions) 7ms 10ms 12ms 13ms 0ms 3.0s
singleString (stringField) 7ms 10ms 13ms 25ms 0ms 4.7s
synthetic (title) 18ms 21ms 45ms 132ms 1486ms 9.9s
synthetic (string inside object) 19ms 28ms 72ms 133ms 1556ms 9.4s

📚 Glossary

column definitions

  • benchmark — the name of the test, e.g. "article", followed by the label of the field being measured, e.g. "(title)".
  • latency — the time between when a key was pressed and when it was rendered. derived from a set of samples. the median (p50) is shown to show the most common latency.
  • p75 — the 75th percentile of the input latency in the test run. 75% of the sampled inputs in this benchmark were processed faster than this value. this provides insight into the upper range of typical performance.
  • p90 — the 90th percentile of the input latency in the test run. 90% of the sampled inputs were faster than this. this metric helps identify slower interactions that occurred less frequently during the benchmark.
  • p99 — the 99th percentile of the input latency in the test run. only 1% of sampled inputs were slower than this. this represents the worst-case scenarios encountered during the benchmark, useful for identifying potential performance outliers.
  • blocking time — the total time during which the main thread was blocked, preventing user input and UI updates. this metric helps identify performance bottlenecks that may cause the interface to feel unresponsive.
  • test duration — how long the test run took to complete.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 7, 2026

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 39.63% 26111 / 65883
🔵 Statements 32.06% 33885 / 105692
🔵 Functions 28.43% 5260 / 18498
🔵 Branches 24.54% 20690 / 84303
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/sanity/src/structure/structureBuilder/util/getExtendedProjection.ts 95.31% 92.15% 100% 94.91% 101, 128-132
Generated in workflow #54372 for commit 39a12b2 by the Vitest Coverage Report Action

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes crashes in document list sorting when custom orderings include array index/key access (e.g. items[0].value) by switching from dot-splitting to typed path parsing and by teaching the projection-tree builder how to resolve array member types.

Changes:

  • Parse ordering field paths via PathUtils.fromString() to support bracket notation segments.
  • Extend projection tree building to fold array accessors into node keys and resolve single-member array types (including reference deref).
  • Add unit tests and update test-studio schema with array-based ordering examples for manual verification.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
packages/sanity/src/structure/structureBuilder/util/getExtendedProjection.ts Core logic: parse typed paths, handle array access/keyed segments, build correct GROQ projections.
packages/sanity/src/structure/structureBuilder/util/tests/getExtendedProjection.test.ts Adds coverage for array index/keyed access, mixed orderings, and strict-mode error paths.
dev/test-studio/schema/standard/withObjectFieldsOrder.ts Adds relatedAuthors/tags arrays and corresponding array-based orderings for manual testing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 29 to 33
const node = nodes.get(fieldName)
if (node) return node
const createdNode: ProjectionNode = {reference, children: new Map()}
nodes.set(fieldName, createdNode)
return createdNode
Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

getOrCreateChildNode never upgrades an existing node from reference: false to reference: true. This can produce an incorrect projection when the same node key is first inserted as a leaf (or as a non-reference) and later needs to be rendered as a reference with children (eg items[0] plus items[0].value would end up as items[0]{value} instead of items[0]->{value}). Consider updating the existing node in-place when reference is requested and the stored node is non-reference (eg node.reference ||= reference).

Copilot uses AI. Check for mistakes.
Comment on lines +100 to 103
if (!head || typeof head !== 'string' || !('fields' in schemaType)) {
return
}

Copy link

Copilot AI Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In strict mode, attempting to traverse into a non-object schema type currently fails silently because joinReferences returns early when !('fields' in schemaType). This means invalid orderings like publicationYear.foo (or items[0].value where the array member is primitive) won't be rejected even though getExtendedProjection(..., strict=true) is used for validation. Consider calling reportError when strict is true and a remaining path segment is being applied to a schema type without fields.

Suggested change
if (!head || typeof head !== 'string' || !('fields' in schemaType)) {
return
}
if (!head || typeof head !== 'string') {
return
}
if (!('fields' in schemaType)) {
reportError(
`The current ordering config attempted to traverse into field "${head}" on non-object schema type "${schemaType.name}"`,
strict,
)
return
}

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants