diff --git a/dev/test-create-integration-studio/.depcheckignore.json b/dev/test-create-integration-studio/.depcheckignore.json new file mode 100644 index 00000000000..0958cae05dc --- /dev/null +++ b/dev/test-create-integration-studio/.depcheckignore.json @@ -0,0 +1,3 @@ +{ + "ignore": ["styled-components", "react", "react-dom", "sanity"] +} diff --git a/dev/test-create-integration-studio/.gitignore b/dev/test-create-integration-studio/.gitignore new file mode 100644 index 00000000000..9b1c8b133c9 --- /dev/null +++ b/dev/test-create-integration-studio/.gitignore @@ -0,0 +1 @@ +/dist diff --git a/dev/test-create-integration-studio/package.json b/dev/test-create-integration-studio/package.json new file mode 100644 index 00000000000..ad3b6ccf5cd --- /dev/null +++ b/dev/test-create-integration-studio/package.json @@ -0,0 +1,22 @@ +{ + "name": "test-create-integration-studio", + "version": "3.61.0", + "private": true, + "license": "MIT", + "author": "Sanity.io ", + "scripts": { + "build": "../.bin/sanity build", + "clean": "rimraf .sanity dist", + "deploy": "npx sanity deploy", + "dev": "../.bin/sanity dev", + "lint": "eslint .", + "start": "../.bin/sanity start" + }, + "dependencies": { + "@sanity/code-input": "^4.1.4", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "sanity": "workspace:*", + "styled-components": "^6.1.0" + } +} diff --git a/dev/test-create-integration-studio/sanity.cli.ts b/dev/test-create-integration-studio/sanity.cli.ts new file mode 100644 index 00000000000..89fd9fa0307 --- /dev/null +++ b/dev/test-create-integration-studio/sanity.cli.ts @@ -0,0 +1,11 @@ +import {defineCliConfig} from 'sanity/cli' + +export default defineCliConfig({ + api: { + projectId: 'ppsg7ml5', + dataset: 'test', + }, + + studioHost: 'create-integration-test', + autoUpdates: false, +}) diff --git a/dev/test-create-integration-studio/sanity.config.ts b/dev/test-create-integration-studio/sanity.config.ts new file mode 100644 index 00000000000..146861de727 --- /dev/null +++ b/dev/test-create-integration-studio/sanity.config.ts @@ -0,0 +1,22 @@ +// eslint-disable-next-line import/no-extraneous-dependencies +import {codeInput} from '@sanity/code-input' +import {defineConfig} from 'sanity' +import {structureTool} from 'sanity/structure' + +import {schemaTypes} from './schema' + +export default defineConfig({ + plugins: [structureTool(), codeInput()], + title: 'Strict', + name: 'default', + projectId: 'ppsg7ml5', + dataset: 'test', + schema: {types: schemaTypes}, + + beta: { + create: { + startInCreateEnabled: true, + fallbackStudioOrigin: 'create-integration-test.sanity.studio', + }, + }, +}) diff --git a/dev/test-create-integration-studio/schema.ts b/dev/test-create-integration-studio/schema.ts new file mode 100644 index 00000000000..32f16197491 --- /dev/null +++ b/dev/test-create-integration-studio/schema.ts @@ -0,0 +1,354 @@ +import {defineArrayMember, defineField, defineType, type FieldGroupDefinition} from 'sanity' + +export const mainGroup: FieldGroupDefinition = { + name: 'main', + title: 'Common fields', +} +export const seoGroup: FieldGroupDefinition = { + name: 'seo', + title: 'SEO fields', +} + +export const schemaTypes = [ + defineType({ + title: 'Documentation Article', + name: 'article', + type: 'document', + groups: [mainGroup, seoGroup], + fieldsets: [{name: 'switches', title: 'Article settings', options: {columns: 2}}], + fields: [ + defineField({ + name: 'title', + title: 'Title', + type: 'string', + description: 'Main header and page title.', + group: [mainGroup.name], + }), + defineField({ + name: 'description', + title: 'Description', + type: 'text', + rows: 3, + description: 'Lede and page summary.', + group: [mainGroup.name], + }), + defineField({ + type: 'image', + name: 'image', + title: 'Image', + description: 'Primary image for content.', + group: [mainGroup.name], + }), + defineField({ + name: 'authors', + title: 'Authors', + type: 'array', + description: 'One or more content authors', + validation: (Rule) => Rule.required().min(1).unique(), + of: [ + defineArrayMember({ + type: 'object', + name: 'authors', + title: 'Authors', + fields: [ + defineField({ + type: 'string', + name: 'name', + title: 'Full Name', + }), + defineField({ + type: 'string', + name: 'email', + title: 'Email', + }), + ], + }), + ], + group: [mainGroup.name], + }), + { + title: 'Slug', + name: 'slug', + type: 'slug', + description: 'Last part of the page URL.', + options: { + source: 'title', + auto: true, + }, + }, + { + title: 'Hide this article?', + name: 'hidden', + type: 'boolean', + description: 'Turn this on to prevent this document from showing up in search results.', + }, + { + title: 'Enterprise Feature', + name: 'enterprise', + type: 'boolean', + description: 'This article describes a feature only available on enterprise plans', + }, + { + title: 'Experimental Feature', + name: 'experimental', + type: 'boolean', + description: + 'This article describes a feature that should be considered experimental, where the API and feature set might change', + }, + { + title: 'Body', + name: 'body', + type: 'blockContent', + }, + { + title: 'Search keywords', + name: 'keywords', + type: 'array', + of: [{type: 'string'}], + options: { + layout: 'tags', + }, + description: 'A list of keywords to supplement search index.', + }, + { + title: 'Related Articles', + name: 'articles', + type: 'array', + of: [ + { + type: 'reference', + to: [{type: 'article'}], + }, + ], + }, + defineField({ + name: 'seoTitle', + title: 'SEO Title', + type: 'string', + description: 'Will override title used for SEO and social media previews.', + group: [seoGroup.name], + }), + defineField({ + name: 'seoDescription', + title: 'SEO Description', + type: 'text', + rows: 3, + description: 'Will override description used for SEO and social media previews.', + group: [seoGroup.name], + }), + defineField({ + name: 'seoImage', + title: 'SEO Image', + type: 'image', + description: 'Will override image used for SEO and social media previews.', + group: [seoGroup.name], + }), + ], + }), + defineType({ + title: 'Block Content', + name: 'blockContent', + type: 'array', + of: [ + { + title: 'Block', + type: 'block', + styles: [ + {title: 'Normal', value: 'normal'}, + {title: 'H1', value: 'h1'}, + {title: 'H2', value: 'h2'}, + {title: 'H3', value: 'h3'}, + {title: 'H4', value: 'h4'}, + {title: 'Quote', value: 'blockquote'}, + ], + marks: { + decorators: [ + {title: 'Strong', value: 'strong'}, + {title: 'Emphasis', value: 'em'}, + {title: 'Code', value: 'code'}, + ], + annotations: [ + { + title: 'Abbreviation', + name: 'abbreviation', + type: 'object', + description: 'Add definitions for abbreviations, initialisms, and acronyms', + fields: [ + { + title: 'Expansion', + name: 'title', + type: 'string', + description: 'Spell out the full term', + }, + ], + }, + ], + }, + }, + { + title: 'Call to action', + name: 'callToAction', + type: 'object', + fields: [ + { + title: 'Label', + name: 'label', + type: 'string', + }, + { + title: 'Url', + name: 'url', + type: 'string', + }, + ], + }, + { + title: 'Image', + type: 'image', + options: { + hotspot: true, + }, + preview: { + select: { + imageUrl: 'asset.url', + title: 'caption', + }, + }, + fields: [ + { + title: 'Caption', + name: 'caption', + type: 'string', + }, + { + name: 'alt', + type: 'string', + title: 'Alt text', + description: 'Alternative text for screenreaders. Falls back on caption if not set', + }, + { + title: 'Enable lightbox', + description: + '❓ Optional. The default behavior is to enable it if image is large enough to benefit from it.', + name: 'enableLightbox', + type: 'boolean', + }, + { + title: 'Icon', + name: 'isIcon', + type: 'boolean', + }, + { + title: 'Disable shadow', + description: 'Not implemented in most surfaces.', + name: 'disableShadow', + type: 'boolean', + }, + { + title: 'Large', + description: 'Not implemented in most surfaces.', + name: 'isLarge', + type: 'boolean', + }, + { + name: 'infoBox', + title: 'Info Box', + type: 'object', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string', + }, + ], + }, + ], + }, + { + name: 'infoBox', + title: 'Info Box', + type: 'object', + fields: [ + { + name: 'title', + title: 'Title', + type: 'string', + }, + { + title: 'Box Content', + name: 'body', + type: 'text', + }, + ], + preview: { + select: { + title: 'title', + body: 'body', + }, + prepare(selection) { + return selection + }, + }, + }, + {name: 'code', type: 'code'}, + { + name: 'protip', + type: 'object', + fields: [ + { + title: 'Protip', + name: 'body', + type: 'text', + }, + ], + preview: { + select: { + body: 'body', + }, + prepare(selection) { + return selection + }, + }, + }, + { + name: 'gotcha', + type: 'object', + fields: [ + { + title: 'Gotcha', + name: 'body', + type: 'text', + }, + ], + preview: { + select: { + body: 'body', + }, + prepare(selection) { + return selection + }, + }, + }, + { + name: 'example', + type: 'object', + fields: [ + { + title: 'Example', + name: 'body', + description: 'Use this to exemplify something that’s not just a code block', + type: 'text', + }, + ], + preview: { + select: { + body: 'body', + }, + prepare(selection) { + return selection + }, + }, + }, + ], + }), +] diff --git a/dev/test-create-integration-studio/tsconfig.json b/dev/test-create-integration-studio/tsconfig.json new file mode 100644 index 00000000000..e10fe0a8eaa --- /dev/null +++ b/dev/test-create-integration-studio/tsconfig.json @@ -0,0 +1,3 @@ +{ + "extends": "../tsconfig.dev" +} diff --git a/packages/@sanity/types/src/reference/types.ts b/packages/@sanity/types/src/reference/types.ts index 8a2e51a00a4..89d3a9ce472 100644 --- a/packages/@sanity/types/src/reference/types.ts +++ b/packages/@sanity/types/src/reference/types.ts @@ -2,6 +2,7 @@ import {type SanityClient} from '@sanity/client' import {type SanityDocument} from '../documents' import {type Path} from '../paths' +import {type BaseSchemaTypeOptions} from '../schema' /** @public */ export interface Reference { @@ -56,7 +57,7 @@ export interface ReferenceFilterQueryOptions { } /** @public */ -export interface ReferenceBaseOptions { +export interface ReferenceBaseOptions extends BaseSchemaTypeOptions { disableNew?: boolean } diff --git a/packages/@sanity/types/src/schema/definition/type/array.ts b/packages/@sanity/types/src/schema/definition/type/array.ts index efecfdc1cfb..2a966859728 100644 --- a/packages/@sanity/types/src/schema/definition/type/array.ts +++ b/packages/@sanity/types/src/schema/definition/type/array.ts @@ -8,12 +8,17 @@ import { type IntrinsicTypeName, type TypeAliasDefinition, } from '../schemaDefinition' -import {type BaseSchemaDefinition, type SearchConfiguration, type TitledListValue} from './common' +import { + type BaseSchemaDefinition, + type BaseSchemaTypeOptions, + type SearchConfiguration, + type TitledListValue, +} from './common' export type {InsertMenuOptions} /** @public */ -export interface ArrayOptions extends SearchConfiguration { +export interface ArrayOptions extends SearchConfiguration, BaseSchemaTypeOptions { list?: TitledListValue[] | V[] // inferring the array.of value for ArrayDefinition cause too much code-noise and was removed. // Since we don't have the type-info needed here, we allow values diff --git a/packages/@sanity/types/src/schema/definition/type/block.ts b/packages/@sanity/types/src/schema/definition/type/block.ts index d1613dd53dd..7c045b7e58c 100644 --- a/packages/@sanity/types/src/schema/definition/type/block.ts +++ b/packages/@sanity/types/src/schema/definition/type/block.ts @@ -3,13 +3,13 @@ import {type ComponentType, type ReactNode} from 'react' import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' import {type ArrayOfType} from './array' -import {type BaseSchemaDefinition} from './common' +import {type BaseSchemaDefinition, type BaseSchemaTypeOptions} from './common' import {type ObjectDefinition} from './object' /** * Schema options for a Block schema definition * @public */ -export interface BlockOptions { +export interface BlockOptions extends BaseSchemaTypeOptions { /** * Turn on or off the builtin browser spellchecking. Default is on. */ diff --git a/packages/@sanity/types/src/schema/definition/type/boolean.ts b/packages/@sanity/types/src/schema/definition/type/boolean.ts index 9c5bf9ca6ae..30c0ef83ffd 100644 --- a/packages/@sanity/types/src/schema/definition/type/boolean.ts +++ b/packages/@sanity/types/src/schema/definition/type/boolean.ts @@ -1,9 +1,9 @@ import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' -import {type BaseSchemaDefinition} from './common' +import {type BaseSchemaDefinition, type BaseSchemaTypeOptions} from './common' /** @public */ -export interface BooleanOptions { +export interface BooleanOptions extends BaseSchemaTypeOptions { layout?: 'switch' | 'checkbox' } diff --git a/packages/@sanity/types/src/schema/definition/type/common.ts b/packages/@sanity/types/src/schema/definition/type/common.ts index 91e1c638965..b970f82e3fe 100644 --- a/packages/@sanity/types/src/schema/definition/type/common.ts +++ b/packages/@sanity/types/src/schema/definition/type/common.ts @@ -24,6 +24,33 @@ export type FieldGroupDefinition = { i18n?: I18nTextRecord<'title'> } +/** + * Options for configuring how Sanity Create interfaces with the type or field. + * + * @public + */ +export interface SanityCreateOptions { + /** Set to true to exclude a type or field from appearing in Sanity Create */ + exclude?: boolean + + /** + * A short description of what the type or field is used for. + * Purpose can be used to improve how and when content mapping uses the field. + * */ + purpose?: string +} + +/** + * `BaseOptions` applies to all type options. + * + * It can be extended by interface declaration merging in plugins to provide generic options to all types and fields. + * + * @public + * */ +export interface BaseSchemaTypeOptions { + sanityCreate?: SanityCreateOptions +} + /** @public */ export interface BaseSchemaDefinition { name: string diff --git a/packages/@sanity/types/src/schema/definition/type/date.ts b/packages/@sanity/types/src/schema/definition/type/date.ts index 7a234ffc00b..b76e17009dd 100644 --- a/packages/@sanity/types/src/schema/definition/type/date.ts +++ b/packages/@sanity/types/src/schema/definition/type/date.ts @@ -1,10 +1,10 @@ import {type FieldReference} from '../../../validation' import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' -import {type BaseSchemaDefinition} from './common' +import {type BaseSchemaDefinition, type BaseSchemaTypeOptions} from './common' /** @public */ -export interface DateOptions { +export interface DateOptions extends BaseSchemaTypeOptions { dateFormat?: string } diff --git a/packages/@sanity/types/src/schema/definition/type/datetime.ts b/packages/@sanity/types/src/schema/definition/type/datetime.ts index dcc102b2112..7cc85c042f0 100644 --- a/packages/@sanity/types/src/schema/definition/type/datetime.ts +++ b/packages/@sanity/types/src/schema/definition/type/datetime.ts @@ -1,10 +1,10 @@ import {type FieldReference} from '../../../validation' import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' -import {type BaseSchemaDefinition} from './common' +import {type BaseSchemaDefinition, type BaseSchemaTypeOptions} from './common' /** @public */ -export interface DatetimeOptions { +export interface DatetimeOptions extends BaseSchemaTypeOptions { dateFormat?: string timeFormat?: string timeStep?: number diff --git a/packages/@sanity/types/src/schema/definition/type/document.ts b/packages/@sanity/types/src/schema/definition/type/document.ts index 0783ada5cee..e567dd18b42 100644 --- a/packages/@sanity/types/src/schema/definition/type/document.ts +++ b/packages/@sanity/types/src/schema/definition/type/document.ts @@ -1,6 +1,7 @@ import {type SanityDocument} from '../../../documents/types' import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty, type SortOrdering} from '../../types' +import {type BaseSchemaTypeOptions} from './common' import {type ObjectDefinition} from './object' /** @@ -9,7 +10,7 @@ import {type ObjectDefinition} from './object' * @public */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface DocumentOptions {} +export interface DocumentOptions extends BaseSchemaTypeOptions {} /** @public */ // eslint-disable-next-line @typescript-eslint/no-empty-interface diff --git a/packages/@sanity/types/src/schema/definition/type/email.ts b/packages/@sanity/types/src/schema/definition/type/email.ts index d97efbbedd6..001c2a7672a 100644 --- a/packages/@sanity/types/src/schema/definition/type/email.ts +++ b/packages/@sanity/types/src/schema/definition/type/email.ts @@ -1,6 +1,6 @@ import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' -import {type BaseSchemaDefinition} from './common' +import {type BaseSchemaDefinition, type BaseSchemaTypeOptions} from './common' /** @public */ // eslint-disable-next-line @typescript-eslint/no-empty-interface @@ -9,7 +9,7 @@ export interface EmailRule extends RuleDef {} /** @public */ // only exists to support declaration extensions // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface EmailOptions {} +export interface EmailOptions extends BaseSchemaTypeOptions {} /** @public */ export interface EmailDefinition extends BaseSchemaDefinition { diff --git a/packages/@sanity/types/src/schema/definition/type/geopoint.ts b/packages/@sanity/types/src/schema/definition/type/geopoint.ts index fa505eac894..2e3d00bfac3 100644 --- a/packages/@sanity/types/src/schema/definition/type/geopoint.ts +++ b/packages/@sanity/types/src/schema/definition/type/geopoint.ts @@ -1,6 +1,6 @@ import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' -import {type BaseSchemaDefinition} from './common' +import {type BaseSchemaDefinition, type BaseSchemaTypeOptions} from './common' /** * Geographical point representing a pair of latitude and longitude coordinates, @@ -37,7 +37,7 @@ export interface GeopointRule extends RuleDef {} /** @public */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface GeopointOptions {} +export interface GeopointOptions extends BaseSchemaTypeOptions {} /** @public */ export interface GeopointDefinition extends BaseSchemaDefinition { diff --git a/packages/@sanity/types/src/schema/definition/type/number.ts b/packages/@sanity/types/src/schema/definition/type/number.ts index 19082922544..939383f240c 100644 --- a/packages/@sanity/types/src/schema/definition/type/number.ts +++ b/packages/@sanity/types/src/schema/definition/type/number.ts @@ -1,11 +1,11 @@ import {type FieldReference} from '../../../validation' import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' -import {type BaseSchemaDefinition, type EnumListProps} from './common' +import {type BaseSchemaDefinition, type BaseSchemaTypeOptions, type EnumListProps} from './common' /** @public */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface NumberOptions extends EnumListProps {} +export interface NumberOptions extends EnumListProps, BaseSchemaTypeOptions {} /** @public */ export interface NumberRule extends RuleDef { diff --git a/packages/@sanity/types/src/schema/definition/type/object.ts b/packages/@sanity/types/src/schema/definition/type/object.ts index 3f8d94226b0..443bdf5d830 100644 --- a/packages/@sanity/types/src/schema/definition/type/object.ts +++ b/packages/@sanity/types/src/schema/definition/type/object.ts @@ -4,12 +4,13 @@ import {type InitialValueProperty} from '../../types' import {type FieldDefinition} from '../schemaDefinition' import { type BaseSchemaDefinition, + type BaseSchemaTypeOptions, type FieldGroupDefinition, type FieldsetDefinition, } from './common' /** @public */ -export interface ObjectOptions { +export interface ObjectOptions extends BaseSchemaTypeOptions { collapsible?: boolean collapsed?: boolean columns?: number diff --git a/packages/@sanity/types/src/schema/definition/type/slug.ts b/packages/@sanity/types/src/schema/definition/type/slug.ts index ca6edcc0242..cf1accd73d9 100644 --- a/packages/@sanity/types/src/schema/definition/type/slug.ts +++ b/packages/@sanity/types/src/schema/definition/type/slug.ts @@ -3,7 +3,7 @@ import {type SlugifierFn, type SlugSourceFn} from '../../../slug' import {type SlugIsUniqueValidator} from '../../../validation' import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' -import {type BaseSchemaDefinition} from './common' +import {type BaseSchemaDefinition, type BaseSchemaTypeOptions} from './common' /** @public */ export interface SlugValue { @@ -16,7 +16,7 @@ export interface SlugValue { export interface SlugRule extends RuleDef {} /** @public */ -export interface SlugOptions { +export interface SlugOptions extends BaseSchemaTypeOptions { source?: string | Path | SlugSourceFn maxLength?: number slugify?: SlugifierFn diff --git a/packages/@sanity/types/src/schema/definition/type/string.ts b/packages/@sanity/types/src/schema/definition/type/string.ts index 0b708079e3b..dd7ed8034eb 100644 --- a/packages/@sanity/types/src/schema/definition/type/string.ts +++ b/packages/@sanity/types/src/schema/definition/type/string.ts @@ -1,11 +1,19 @@ import {type FieldReference} from '../../../validation' import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' -import {type BaseSchemaDefinition, type EnumListProps, type SearchConfiguration} from './common' +import { + type BaseSchemaDefinition, + type BaseSchemaTypeOptions, + type EnumListProps, + type SearchConfiguration, +} from './common' /** @public */ // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface StringOptions extends EnumListProps, SearchConfiguration {} +export interface StringOptions + extends EnumListProps, + SearchConfiguration, + BaseSchemaTypeOptions {} /** @public */ export interface StringRule extends RuleDef { diff --git a/packages/@sanity/types/src/schema/definition/type/url.ts b/packages/@sanity/types/src/schema/definition/type/url.ts index a8c6e2554bc..906314b21fd 100644 --- a/packages/@sanity/types/src/schema/definition/type/url.ts +++ b/packages/@sanity/types/src/schema/definition/type/url.ts @@ -1,7 +1,7 @@ import {type UriValidationOptions} from '../../../validation/types' import {type RuleDef, type ValidationBuilder} from '../../ruleBuilder' import {type InitialValueProperty} from '../../types' -import {type BaseSchemaDefinition} from './common' +import {type BaseSchemaDefinition, type BaseSchemaTypeOptions} from './common' /** @public */ export interface UrlRule extends RuleDef { @@ -11,7 +11,7 @@ export interface UrlRule extends RuleDef { /** @public */ // only exists to support declaration extensions // eslint-disable-next-line @typescript-eslint/no-empty-interface -export interface UrlOptions {} +export interface UrlOptions extends BaseSchemaTypeOptions {} /** @public */ export interface UrlDefinition extends BaseSchemaDefinition { diff --git a/packages/@sanity/types/src/user/types.ts b/packages/@sanity/types/src/user/types.ts index e1bbbaeb627..1d90def9cbc 100644 --- a/packages/@sanity/types/src/user/types.ts +++ b/packages/@sanity/types/src/user/types.ts @@ -23,4 +23,7 @@ export interface User { displayName?: string imageUrl?: string email?: string + + /** global sanity user id */ + sanityUserId?: string } diff --git a/packages/@sanity/types/test/boolean.test.ts b/packages/@sanity/types/test/boolean.test.ts index 1ba1a92abc8..5cb0fc796a3 100644 --- a/packages/@sanity/types/test/boolean.test.ts +++ b/packages/@sanity/types/test/boolean.test.ts @@ -29,6 +29,9 @@ describe('boolean types', () => { hidden: () => false, options: { layout: 'checkbox', + sanityCreate: { + exclude: true, + }, }, }) diff --git a/packages/sanity/src/_singletons/context/SanityCreateConfigContext.tsx b/packages/sanity/src/_singletons/context/SanityCreateConfigContext.tsx new file mode 100644 index 00000000000..2f89b9833fa --- /dev/null +++ b/packages/sanity/src/_singletons/context/SanityCreateConfigContext.tsx @@ -0,0 +1,13 @@ +import {createContext} from 'sanity/_createContext' + +import type {SanityCreateConfigContextValue} from '../../core' + +/** + * @internal + */ +export const SanityCreateConfigContext = createContext( + 'sanity/_singletons/context/start-in-create-enabled', + { + startInCreateEnabled: false, + }, +) diff --git a/packages/sanity/src/_singletons/index.ts b/packages/sanity/src/_singletons/index.ts index 05761b9609f..6679e2e0282 100644 --- a/packages/sanity/src/_singletons/index.ts +++ b/packages/sanity/src/_singletons/index.ts @@ -47,6 +47,7 @@ export * from './context/ResourceCacheContext' export * from './context/ReviewChangesContext' export * from './context/RouterContext' export * from './context/RouterHistoryContext' +export * from './context/SanityCreateConfigContext' export * from './context/ScheduledPublishingEnabledContext' export * from './context/SchedulePublishingUpsellContext' export * from './context/Schedules' diff --git a/packages/sanity/src/core/config/__tests__/resolveConfig.test.ts b/packages/sanity/src/core/config/__tests__/resolveConfig.test.ts index 34cb13bea71..7eaed5a6dc6 100644 --- a/packages/sanity/src/core/config/__tests__/resolveConfig.test.ts +++ b/packages/sanity/src/core/config/__tests__/resolveConfig.test.ts @@ -162,6 +162,7 @@ describe('resolveConfig', () => { {name: 'sanity/comments'}, {name: 'sanity/tasks'}, {name: 'sanity/scheduled-publishing'}, + {name: 'sanity/create-integration'}, ]) }) @@ -188,6 +189,7 @@ describe('resolveConfig', () => { expect(workspace.__internal.options.plugins).toMatchObject([ {name: 'sanity/comments'}, {name: 'sanity/tasks'}, + {name: 'sanity/create-integration'}, ]) }) }) diff --git a/packages/sanity/src/core/config/configPropertyReducers.ts b/packages/sanity/src/core/config/configPropertyReducers.ts index c2b6a66205a..221ad852b12 100644 --- a/packages/sanity/src/core/config/configPropertyReducers.ts +++ b/packages/sanity/src/core/config/configPropertyReducers.ts @@ -391,3 +391,48 @@ export const legacySearchEnabledReducer: ConfigPropertyReducer { + const {config, initialValue} = opts + const flattenedConfig = flattenConfig(config, []) + + const result = flattenedConfig.reduce((acc, {config: innerConfig}) => { + const resolver = innerConfig.beta?.create?.startInCreateEnabled + + if (!resolver && typeof resolver !== 'boolean') return acc + if (typeof resolver === 'boolean') return resolver + + throw new Error( + `Expected \`beta.create.startInCreateEnabled\` to be a boolean, but received ${getPrintableType( + resolver, + )}`, + ) + }, initialValue) + + return result +} + +export const createFallbackOriginReducer = (config: PluginOptions): string | undefined => { + const flattenedConfig = flattenConfig(config, []) + + const result = flattenedConfig.reduce( + (acc, {config: innerConfig}) => { + const resolver = innerConfig.beta?.create?.fallbackStudioOrigin + + if (!resolver) return acc + if (typeof resolver === 'string') return resolver + + throw new Error( + `Expected \`beta.create.fallbackStudioOrigin\` to be a string, but received ${getPrintableType( + resolver, + )}`, + ) + }, + undefined as string | undefined, + ) + + return result +} diff --git a/packages/sanity/src/core/config/prepareConfig.ts b/packages/sanity/src/core/config/prepareConfig.ts index 054250acc5b..ac7f68dc32a 100644 --- a/packages/sanity/src/core/config/prepareConfig.ts +++ b/packages/sanity/src/core/config/prepareConfig.ts @@ -14,6 +14,7 @@ import { import {isValidElementType} from 'react-is' import {map, shareReplay} from 'rxjs/operators' +import {getStartInCreateSortedActions} from '../create/getStartInCreateSortedActions' import {FileSource, ImageSource} from '../form/studio/assetSource' import {type LocaleSource} from '../i18n' import {prepareI18n} from '../i18n/i18nConfig' @@ -25,6 +26,7 @@ import {operatorDefinitions} from '../studio/components/navbar/search/definition import {type InitialValueTemplateItem, type Template, type TemplateItem} from '../templates' import {EMPTY_ARRAY, isNonNullable} from '../util' import { + createFallbackOriginReducer, documentActionsReducer, documentBadgesReducer, documentCommentsEnabledReducer, @@ -42,6 +44,7 @@ import { partialIndexingEnabledReducer, resolveProductionUrlReducer, schemaTemplatesReducer, + startInCreateEnabledReducer, toolsReducer, } from './configPropertyReducers' import {ConfigResolutionError} from './ConfigResolutionError' @@ -495,14 +498,16 @@ function resolveSource({ config, }), document: { - actions: (partialContext) => - resolveConfigProperty({ + actions: (partialContext) => { + const actions = resolveConfigProperty({ config, context: {...context, ...partialContext}, initialValue: initialDocumentActions, propertyName: 'document.actions', reducer: documentActionsReducer, - }), + }) + return getStartInCreateSortedActions(actions) + }, badges: (partialContext) => resolveConfigProperty({ config, @@ -648,6 +653,10 @@ function resolveSource({ // This beta feature is no longer available. enabled: false, }, + create: { + startInCreateEnabled: startInCreateEnabledReducer({config, initialValue: true}), + fallbackStudioOrigin: createFallbackOriginReducer(config), + }, }, } diff --git a/packages/sanity/src/core/config/resolveDefaultPlugins.ts b/packages/sanity/src/core/config/resolveDefaultPlugins.ts index 2e79260b243..978fb8aa20d 100644 --- a/packages/sanity/src/core/config/resolveDefaultPlugins.ts +++ b/packages/sanity/src/core/config/resolveDefaultPlugins.ts @@ -1,4 +1,5 @@ import {comments} from '../comments/plugin' +import {createIntegration} from '../create/createIntegration' import {DEFAULT_SCHEDULED_PUBLISH_PLUGIN_OPTIONS} from '../scheduledPublishing/constants' import {SCHEDULED_PUBLISHING_NAME, scheduledPublishing} from '../scheduledPublishing/plugin' import {tasks, TASKS_NAME} from '../tasks/plugin' @@ -9,7 +10,7 @@ import { type WorkspaceOptions, } from './types' -const defaultPlugins = [comments(), tasks(), scheduledPublishing()] +const defaultPlugins = [comments(), tasks(), scheduledPublishing(), createIntegration()] export function getDefaultPlugins( options: DefaultPluginsWorkspaceOptions, diff --git a/packages/sanity/src/core/config/types.ts b/packages/sanity/src/core/config/types.ts index 5903b68b763..2d1beb7db52 100644 --- a/packages/sanity/src/core/config/types.ts +++ b/packages/sanity/src/core/config/types.ts @@ -388,10 +388,12 @@ export interface PluginOptions { */ enableLegacySearch?: boolean } + /** Configuration for studio beta features. * @internal */ beta?: BetaFeatures + /** Configuration for error handling. * @beta */ @@ -917,4 +919,41 @@ interface BetaFeatures { */ enabled: boolean } + + /** + * @beta + */ + create?: { + /** + * When true, a "Start in Sanity Create" action will be shown for all new documents, in place of regular document actions, + * when the following are true: + * - the origin of the current url is listed under Studios in sanity.to/manage (OR fallbackStudioOrigin is provided) + * - [origin]/static/create-manifest.json is available over HTTP GET + * + * The manifest file is automatically created and deployed when deploying studios with `sanity deploy` + * + * @see #fallbackStudioOrigin + */ + startInCreateEnabled?: boolean + + /** + * To show the "Start in Create" button on localhost, or in studios not listed under Studios in sanity.io/manage + * provide a fallback origin as a string. + * + * The string must be the exactly equal `name` as shown for the Studio in manage, and the studio must have create-manifest.json available. + * + * If the provided fallback Studio does not expose create-manifest.json "Start in Sanity Create" will fail when using the fallback. + * + * Example: `wonderful.sanity.studio` + * + * Keep in mind that when fallback origin is used, Sanity Create will used the schema types and dataset in the *deployed* Studio, + * not from localhost. + * + * To see data synced from Sanity Create in your localhost Studio, you must ensure that the deployed fallback studio uses the same + * workspace and schemas as your local configuration. + * + * @see #startInCreateEnabled + */ + fallbackStudioOrigin?: string + } } diff --git a/packages/sanity/src/core/create/__telemetry__/create.telemetry.ts b/packages/sanity/src/core/create/__telemetry__/create.telemetry.ts new file mode 100644 index 00000000000..8d2473db31e --- /dev/null +++ b/packages/sanity/src/core/create/__telemetry__/create.telemetry.ts @@ -0,0 +1,32 @@ +import {defineEvent} from '@sanity/telemetry' + +export const StartInCreateClicked = defineEvent({ + name: 'Start in Create clicked', + version: 1, + description: 'The "Start in Sanity Create" button is clicked.', +}) + +export const StartInCreateAccepted = defineEvent({ + name: 'Start in Create accepted', + version: 1, + description: + 'Continue in the "Start in Sanity Create" dialog was pressed, or auto-confirm was enabled.', +}) + +export const CreateUnlinkClicked = defineEvent({ + name: 'Create Unlink clicked', + version: 1, + description: 'The Unlink action was clicked', +}) + +export const CreateUnlinkAccepted = defineEvent({ + name: 'Create Unlink accepted', + version: 1, + description: 'User confirmed that they want the Studio document unlinked', +}) + +export const EditInCreateClicked = defineEvent({ + name: 'Edit in Create clicked', + version: 1, + description: 'User clicked "Edit in Create"', +}) diff --git a/packages/sanity/src/core/create/components/CreateIntegrationWrapper.tsx b/packages/sanity/src/core/create/components/CreateIntegrationWrapper.tsx new file mode 100644 index 00000000000..9d6fb12b88e --- /dev/null +++ b/packages/sanity/src/core/create/components/CreateIntegrationWrapper.tsx @@ -0,0 +1,6 @@ +import {type LayoutProps} from '../../config' +import {SanityCreateConfigProvider} from '../context/SanityCreateConfigProvider' + +export function CreateIntegrationWrapper(props: LayoutProps) { + return {props.renderDefault(props)} +} diff --git a/packages/sanity/src/core/create/components/CreateLearnMoreButton.tsx b/packages/sanity/src/core/create/components/CreateLearnMoreButton.tsx new file mode 100644 index 00000000000..570567776e7 --- /dev/null +++ b/packages/sanity/src/core/create/components/CreateLearnMoreButton.tsx @@ -0,0 +1,26 @@ +import {LaunchIcon} from '@sanity/icons' +import {type ForwardedRef, forwardRef} from 'react' + +import {Button} from '../../../ui-components' +import {useTranslation} from '../../i18n' +import {createLocaleNamespace} from '../i18n' + +export const CreateLearnMoreButton = forwardRef(function CreateLearnMoreButton( + props, + ref: ForwardedRef, +) { + const {t} = useTranslation(createLocaleNamespace) + // TODO - get the correct link + return ( +