sanity icon indicating copy to clipboard operation
sanity copied to clipboard

feat: adds support for Create-Studio integration

Open snorrees opened this issue 1 year ago • 5 comments

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.

image

Clicking it will open the following dialog (gfx pending):

image

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):

image

On the link is established, the document will be readOnly and the pane footer will be in a special "Create linked" mode:

image

Clicking the info icon will open a popover (gfx pending).

Hover tooltip: image

Click popover: image

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:

image

"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:

  1. implement as much functionally as possible using our public plugin APIs
  2. keep as much Create integration code under the same directory as possible
  3. 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

  • exclude removes a type or field from appearing in Create. Excluded document types will not have a "Start in Sanity Create" button.
  • purpose supersedes description, 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, 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)
  • 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-json on 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.

snorrees avatar Oct 19 '24 13:10 snorrees

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

vercel[bot] avatar Oct 19 '24 13:10 vercel[bot]

No changes to documentation

github-actions[bot] avatar Oct 19 '24 15:10 github-actions[bot]

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

github-actions[bot] avatar Oct 19 '24 15:10 github-actions[bot]

⚡️ 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.

github-actions[bot] avatar Oct 19 '24 15:10 github-actions[bot]

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

View full report↗︎

socket-security[bot] avatar Oct 20 '24 13:10 socket-security[bot]

PR and description updated with a polish-pass by Rob 🙇

snorrees avatar Oct 22 '24 18:10 snorrees

👋 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 avatar Oct 24 '24 12:10 RitaDias

@RitaDias button location: it's a product decision ordered by Even and signed for by Magnus. Yes, default true, too.

snorrees avatar Oct 24 '24 12:10 snorrees

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.

snorrees avatar Oct 24 '24 12:10 snorrees

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.

robinpyon avatar Oct 24 '24 12:10 robinpyon

@RitaDias banner wrapped in Banner 😅 image

snorrees avatar Oct 24 '24 14:10 snorrees

@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.

snorrees avatar Oct 24 '24 19:10 snorrees