sanity
sanity copied to clipboard
feat: adds support for Create-Studio integration
Description
This PR introduces the Studio side of the upcoming Create <-> Studio integration.
It allows users start their content editing journey in Create for new documents, by linking a Create document to a Studio document.
While linked, Create will sync content into the Studio document in real-time. Create linked Studio documents are read-only. Studio users can unlink the document at any point, to continue editing in the Studio. At this point Create will no longer sync content into the Studio document..
What to review
Read through this full PR text before jumping into the code. Changes affect:
- core
- change to userStore needs extra attention
- structure
- schemaType options
There might be some controversial changes in here. I have made PR comments everywhere where I think there might be some eyebrows raised, but there might be more. I'm very much open to course corrections.
Feature details
TODO: link to Notion doc with screen recording of the full Create<->Studio experience.
In Studio deployments with an exposed create-manifest.json, new, prestine documents will have a "Start in Sanity Create" button in the document pane footer. See previous manifest PR for context.
Clicking it will open the following dialog (gfx pending):
Clicking "Learn more" will open a "How to Create<->Studio" article.
Clicking "Continue" will open a new tab which kicks you off to Create which will:
- Create a new document
- Populate it with some initial data
- Link the Create document to the Studio document
A studio document is considered "Create linked" whilst it has _create.ejected === false.
While Create setting up the link (which entails setting the _create property), the following dialog will be shown after clicking "Continue" (copy pending):
On the link is established, the document will be readOnly and the pane footer will be in a special "Create linked" mode:
Clicking the info icon will open a popover (gfx pending).
Hover tooltip:
Click popover:
Clicking anywhere outside closes the popover.
Clicking "Edit in Sanity Create" will open up the Create document that points to this Studio document (using the same target everywhere, so you only will have one Create tab)
Clicking "Unlink" will show the following dialog:
"Unlink now" will unset the _create property, thereby severing the link and making the document a regular Studio document. It will no longer be readOnly.
High level implementation details
Code organization
I tried to:
- implement as much functionally as possible using our public plugin APIs
- keep as much Create integration code under the same directory as possible
- Build on existing principles when extending type options
For 1: this feature has some requirements that where not 100% plugin-supported, and required changes in core and structure:
- document actions sort order
- a clean way to put a pane-wide banner under the document pane header
- a way to force certain "document footer actions" when Create linked
For 2: I had to introduce a bit of indirection to make it possible to render stuff in structure. SanityCreateConfigContextcontains two component implementations used by structure, provided from core.
These are the components rendered when a document is Create linked. (Read only banner & Unlink actions).
For 3: I have added a BaseSchemaTypeOptions type. I always regretted not adding that for the initial v3 launch, since it allows plugins to add generic type-options much more easily. For instance, it could simplify the AI Assist type extensions quite a bit.
SanityCreateOptions
I have added sanityCreate options directly to BaseSchemaTypeOptions. If it feels controversial to have them there, I can move them into a module declaration extension in core, but I feel that is only adding indirection with no upside.
The idea is that SanityCreateOptions will be the DSL with which devs tailor their schema in Create (via the manifest file).
Atm it supports exclude and purpose
excluderemoves a type or field from appearing in Create. Excluded document types will not have a "Start in Sanity Create" button.purposesupersedesdescription, and will be used as metadata when describing the schema to an LLM
We need different options from AI Assist (which also has this exclude option), as Create has different needs than AI Assist.
Start in Create
We show the "Start in Sanity Create" button when:
beta.create.startInCreateEnabled: true. Atm,startInCreateEnableddefaults to true. We might want to flip this tofalsebefore merging, depending on what Product wants.- the document is new (ie, no
_createdAt) - the current browser origin matches a Studio origin found in Studios for the current project (sanity.io/manage)
- exception: developers can provide a fallbackStudioOrigin to make the button appear on localhost
- why is origin important?: Create uses Studio appId when preparing the studio link; it needs to import the schema from
create-manifest-jsonon a known url. It will only visit hosts listed under Studios in manage.
documentOptions.sanityCreate.exlude !== true
Create link
A Studio document is considered linked to Create when there exists a _create metadata field on it.
Specifically we check for _create.ejected === false. The idea is that we can keep the Create metadata around if we want to, but in this implementation we unset the full _create field when unlinking.
This ensures that this metadata does not end up in published documents.
If we want to do "soft unlinks" to keep a trace back to Create in the future (possibly opt-in via config), we can do that with with _create.ejected: true.
Create link readOnly mode will always be enabled, regardless of what beta.create.startInCreateEnabled is.
Getting the global user id
To build the "Start in Sanity Create" url, we need the global user id. For current user, this is currently not available – but it IS for all other userIds. I propose a change to that, by no longer pre-priming the userStore with currentUser (at the cost of one more network request).
See PR code comment for change to userStore.
App-id cache
To build "Start in Sanity Create" url, we need the appId (deployed studio id) from <project>/user-applications.
It is ok to cache this info for the duration of the Studio lifetime (ie, until browser refresh).
I dont know if there are existing caching mechanisms in core I could use instead of rollinga bespoke fetch-cache for this.
Telemetry
I've added telemetry for the following actions:
- Start in Create clicked
- Start in Create accepted (clicking continue or auto-accept is the same event)
- Unlink clicked
- Unlink accepted
- Edit in Create clicked
Open questions
- Do these code-paths need e2e tests?
Known caveats
Create does not respect initialValues, so these will be nuked when Create starts syncing.
TODOs before opening the PR for review
- add unit tests to easily testable code
- address all my own PR todos
Testing
Beware: Cannot be fully tested until we release the Create side of the integration (Studio link/content mapping).
Locally this integration can be tested by running the new test studio: dev/test-create-integration-studio. It has fallbackOrigin set, and will therefor have "Start in Sanity Create" on localhost.
The integration can also be tested on https://create-integration-test.sanity.studio – it has been deployed from this branch.
Notes for release
The Create <-> Studio integration needs a full documentation article. There are a lot of moving parts. Depending on when this goes out, we might want to stealth launch it without a release note.
The latest updates on your projects. Learn more about Vercel for Git ↗︎
| Name | Status | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| page-building-studio | ✅ Ready (Inspect) | Visit Preview | 💬 Add feedback | Oct 24, 2024 8:11pm |
| performance-studio | ✅ Ready (Inspect) | Visit Preview | 💬 Add feedback | Oct 24, 2024 8:11pm |
| test-compiled-studio | ✅ Ready (Inspect) | Visit Preview | 💬 Add feedback | Oct 24, 2024 8:11pm |
| test-next-studio | ✅ Ready (Inspect) | Visit Preview | 💬 Add feedback | Oct 24, 2024 8:11pm |
| test-studio | ✅ Ready (Inspect) | Visit Preview | 💬 Add feedback | Oct 24, 2024 8:11pm |
1 Skipped Deployment
| Name | Status | Preview | Comments | Updated (UTC) |
|---|---|---|---|---|
| studio-workshop | ⬜️ Ignored (Inspect) | Visit Preview | Oct 24, 2024 8:11pm |
No changes to documentation
Component Testing Report Updated Oct 24, 2024 8:07 PM (UTC)
❌ Failed Tests (1) -- expand for details
| File | Status | Duration | Passed | Skipped | Failed |
|---|---|---|---|---|---|
| comments/CommentInput.spec.tsx | ✅ Passed (Inspect) | 45s | 15 | 0 | 0 |
| formBuilder/ArrayInput.spec.tsx | ✅ Passed (Inspect) | 9s | 3 | 0 | 0 |
| formBuilder/inputs/PortableText/Annotations.spec.tsx | ✅ Passed (Inspect) | 31s | 6 | 0 | 0 |
| formBuilder/inputs/PortableText/copyPaste/CopyPaste.spec.tsx | ✅ Passed (Inspect) | 38s | 11 | 7 | 0 |
| formBuilder/inputs/PortableText/copyPaste/CopyPasteFields.spec.tsx | ✅ Passed (Inspect) | 0s | 0 | 12 | 0 |
| formBuilder/inputs/PortableText/Decorators.spec.tsx | ✅ Passed (Inspect) | 18s | 6 | 0 | 0 |
| formBuilder/inputs/PortableText/DisableFocusAndUnset.spec.tsx | ✅ Passed (Inspect) | 11s | 3 | 0 | 0 |
| formBuilder/inputs/PortableText/DragAndDrop.spec.tsx | ✅ Passed (Inspect) | 3m 0s | 0 | 0 | 0 |
| formBuilder/inputs/PortableText/FocusTracking.spec.tsx | ✅ Passed (Inspect) | 45s | 15 | 0 | 0 |
| formBuilder/inputs/PortableText/Input.spec.tsx | ❌ Failed (Inspect) | 1m 42s | 20 | 0 | 1 |
| formBuilder/inputs/PortableText/ObjectBlock.spec.tsx | ✅ Passed (Inspect) | 1m 17s | 18 | 0 | 0 |
| formBuilder/inputs/PortableText/PresenceCursors.spec.tsx | ✅ Passed (Inspect) | 9s | 3 | 9 | 0 |
| formBuilder/inputs/PortableText/RangeDecoration.spec.tsx | ✅ Passed (Inspect) | 27s | 9 | 0 | 0 |
| formBuilder/inputs/PortableText/Styles.spec.tsx | ✅ Passed (Inspect) | 18s | 6 | 0 | 0 |
| formBuilder/inputs/PortableText/Toolbar.spec.tsx | ✅ Passed (Inspect) | 37s | 12 | 0 | 0 |
| formBuilder/tree-editing/TreeEditing.spec.tsx | ✅ Passed (Inspect) | 0s | 0 | 3 | 0 |
| formBuilder/tree-editing/TreeEditingNestedObjects.spec.tsx | ✅ Passed (Inspect) | 0s | 0 | 3 | 0 |
⚡️ Editor Performance Report
Updated Thu, 24 Oct 2024 20:21:04 GMT
| Benchmark | reference latency of sanity@latest |
experiment latency of this branch |
Δ (%) latency difference |
|
|---|---|---|---|---|
| article (title) | 16.9 efps (59ms) | 15.5 efps (65ms) | +6ms (+9.3%) | ✅ |
| article (body) | 58.0 efps (17ms) | 61.5 efps (16ms) | -1ms (-5.8%) | ✅ |
| article (string inside object) | 18.2 efps (55ms) | 16.7 efps (60ms) | +5ms (+9.1%) | ✅ |
| article (string inside array) | 14.4 efps (70ms) | 13.2 efps (76ms) | +7ms (+9.4%) | ✅ |
| recipe (name) | 29.4 efps (34ms) | 28.6 efps (35ms) | +1ms (+2.9%) | ✅ |
| recipe (description) | 34.5 efps (29ms) | 32.3 efps (31ms) | +2ms (+6.9%) | ✅ |
| recipe (instructions) | 99.9+ efps (6ms) | 99.9+ efps (7ms) | +1ms (-/-%) | ✅ |
| synthetic (title) | 14.5 efps (69ms) | 14.3 efps (70ms) | +1ms (+1.4%) | ✅ |
| synthetic (string inside object) | 14.8 efps (68ms) | 14.8 efps (68ms) | +0ms (-/-%) | ✅ |
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 |
|---|---|---|---|---|---|---|
| article (title) | 59ms | 62ms | 69ms | 266ms | 400ms | 15.2s |
| article (body) | 17ms | 20ms | 41ms | 177ms | 328ms | 6.0s |
| article (string inside object) | 55ms | 57ms | 63ms | 201ms | 248ms | 8.3s |
| article (string inside array) | 70ms | 76ms | 114ms | 222ms | 1138ms | 9.8s |
| recipe (name) | 34ms | 35ms | 45ms | 70ms | 0ms | 9.1s |
| recipe (description) | 29ms | 31ms | 35ms | 51ms | 1ms | 5.9s |
| recipe (instructions) | 6ms | 8ms | 8ms | 9ms | 0ms | 3.2s |
| synthetic (title) | 69ms | 72ms | 78ms | 332ms | 1422ms | 17.4s |
| synthetic (string inside object) | 68ms | 73ms | 78ms | 364ms | 991ms | 10.0s |
🧪 Experiment result
The performance result of this branch
| Benchmark | latency | p75 | p90 | p99 | blocking time | test duration |
|---|---|---|---|---|---|---|
| article (title) | 65ms | 69ms | 109ms | 167ms | 767ms | 16.2s |
| article (body) | 16ms | 18ms | 22ms | 254ms | 428ms | 5.6s |
| article (string inside object) | 60ms | 65ms | 73ms | 195ms | 387ms | 8.8s |
| article (string inside array) | 76ms | 83ms | 98ms | 346ms | 1415ms | 10.6s |
| recipe (name) | 35ms | 39ms | 57ms | 82ms | 34ms | 9.1s |
| recipe (description) | 31ms | 33ms | 37ms | 59ms | 1ms | 6.0s |
| recipe (instructions) | 7ms | 9ms | 10ms | 47ms | 0ms | 3.4s |
| synthetic (title) | 70ms | 74ms | 85ms | 139ms | 1623ms | 16.1s |
| synthetic (string inside object) | 68ms | 75ms | 82ms | 629ms | 1777ms | 10.9s |
📚 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.
New dependencies detected. Learn more about Socket for GitHub ↗︎
| Package | New capabilities | Transitives | Size | Publisher |
|---|---|---|---|---|
| npm/@sanity/[email protected] | Transitive: environment | +34 |
15.7 MB | sanity-io |
PR and description updated with a polish-pass by Rob 🙇
👋 I haven't looked at the codey, et but from reading the description I have some thoughts @snorrees
I have added sanityCreate options directly to BaseSchemaTypeOptions. If it feels controversial to have them there, I can move them into a module declaration extension in core, but I feel that is only adding indirection with no upside.
I agree, I don't particular like having them there, however, I also don't see a reason why we shouldn't. While the sanityCreate is not technically "core" I still think we should make the life easier since it's a close tool to the studio.
We show the "Start in Sanity Create" button when:
- beta.create.startInCreateEnabled: true. Atm, startInCreateEnabled defaults to true. We might want to flip this to false before merging, depending on what Product wants.
- the document is new (ie, no _createdAt)
❗ While the first point might be confusing for us that jump in and don't know where it came from, I think it's fine since we can easily turn it off. However, I don't think that "when a document is new" should should it by default. Is there a reason why we didn't put it alongside the rest of the document actions in the ... menu? I'm asking this because, for me, it doesn't make sense that within the studio where the main actions should be around the documents themselves (publishing, discarding etc) is taken over by the linking to a different tool all together simply because a beta property has been turned to true 🤔 Or, at least, this feels strange at first impact
- Do these code-paths need e2e tests?
I'd say no, especially since it spans two different tools I think that e2e tests aren't needed when unit ones exist.
@RitaDias button location: it's a product decision ordered by Even and signed for by Magnus. Yes, default true, too.
Also, do keep in mind the "deployed studio app with manifest needs to exist bit". I know this seems a bit crazy, studios suddently have a button in prod but not in dev.
I'll raise this one more time internally (ie can we ship with default false), but it IS by ordered by decree.
However, I don't think that "when a document is new" should should it by default. Is there a reason why we didn't put it alongside the rest of the document actions in the ... menu? I'm asking this because, for me, it doesn't make sense that within the studio where the main actions should be around the documents themselves (publishing, discarding etc) is taken over by the linking to a different tool all together simply because a beta property has been turned to true 🤔 Or, at least, this feels strange at first impact
@RitaDias
The rationale behind placing this as the primary action is mostly centered around visibility. We did explore putting it in the document actions menu as you've suggested, though it's very easily missed – especially given that we only want to kick off the above flow when the document is in a pristine, untouched state. We did run a few iterations of this by the design team!
It's slightly "visually breaking" in that users who compose a custom document action to appear first will no longer see that immediately, though 1. it will still be accessible in the document actions menu and 2. it will still be listed as first once the document is no longer in a pristine state.
@RitaDias banner wrapped in Banner 😅
@RitaDias I'm happy to report that beta.sanity.startInCreateEnabled will ship false as the default. Ie, it will be OPT IN to have the "Start in Create" button.