Skip to content

Commit

Permalink
wip
Browse files Browse the repository at this point in the history
  • Loading branch information
ricokahler committed Sep 26, 2023
1 parent f7f92bf commit c5407b9
Show file tree
Hide file tree
Showing 74 changed files with 1,736 additions and 1,100 deletions.
1 change: 1 addition & 0 deletions packages/sanity/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
/cli.js
/desk.js
/router.js
/document.js

# Playwright-ct artifacts
/playwright-ct/report
Expand Down
1 change: 1 addition & 0 deletions packages/sanity/exports/document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from '../src/core/document'
11 changes: 11 additions & 0 deletions packages/sanity/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,17 @@
"import": "./lib/index.esm.js",
"default": "./lib/index.esm.js"
},
"./document": {
"types": "./lib/exports/document.d.ts",
"source": "./exports/document.ts",
"require": "./lib/document.js",
"node": {
"import": "./lib/document.cjs.mjs",
"require": "./lib/document.js"
},
"import": "./lib/document.esm.js",
"default": "./lib/document.esm.js"
},
"./_internal": {
"types": "./lib/exports/_internal.d.ts",
"source": "./exports/_internal.ts",
Expand Down
2 changes: 1 addition & 1 deletion packages/sanity/src/core/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export interface Tool<Options = any> {
options?: Options

/**
* The router for the tool. See {@link router.Router}
* The router for the tool. See {@link Router}
*/
router?: Router

Expand Down
10 changes: 10 additions & 0 deletions packages/sanity/src/core/document/DocumentContextError.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/**
* Represents an error thrown when the document context value is missing or not
* found.This error typically indicates that a component is not wrapped in a
* `<DocumentProvider />`.
*/
export class DocumentContextError extends Error {
constructor() {
super('Could not find context value. Did you wrap this component in a <DocumentProvider />?')
}
}
24 changes: 24 additions & 0 deletions packages/sanity/src/core/document/DocumentIdAndTypeContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import {createContext} from 'react'

/**
* The context value type associated with the `DocumentIdAndTypeProvider`.
* @internal
*/
export interface DocumentIdAndTypeContextValue {
/**
* The published ID of the document.
*/
documentId: string
/**
* The resolved document type. If a document type was not given it will be
* resolved async using the `resolveTypeForDocument` method from the document
* store.
*/
documentType: string
}

/**
* The react context associated with the `DocumentIdAndTypeProvider`.
* @internal
*/
export const DocumentIdAndTypeContext = createContext<DocumentIdAndTypeContextValue | null>(null)
96 changes: 96 additions & 0 deletions packages/sanity/src/core/document/DocumentIdAndTypeProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import React, {useEffect, useMemo, useState} from 'react'
import {first} from 'rxjs'
import {getPublishedId} from '../util'
import {useTemplates} from '../hooks'
import {useDocumentStore} from '../store'
import {Template} from '../templates'
import {DocumentIdAndTypeContext, DocumentIdAndTypeContextValue} from './DocumentIdAndTypeContext'

export interface DocumentIdAndTypeProviderProps {
documentId: string
documentType: string | undefined
templateName: string | undefined
children: React.ReactNode
fallback: React.ReactNode
}

export function DocumentIdAndTypeProvider({
fallback,
children,
documentType: typeFromProps,
documentId: idFromProps,
templateName,
}: DocumentIdAndTypeProviderProps) {
const documentId = getPublishedId(idFromProps)
const templates = useTemplates()
const documentStore = useDocumentStore()

const [error, setError] = useState<Error | null>(null)
if (error) throw error

// generate a lookup object for templates by their IDs
const templatesById = useMemo(() => {
return templates.reduce<Record<string, Template>>((acc, t) => {
acc[t.id] = t
return acc
}, {})
}, [templates])

// determines the initial `documentType` based on the following priority:
// 1. the value provided in `typeFromProps`, unless it's a legacy value.
// 2. the schema type associated with the template name in `templatesById`.
// 3. defaults to `null` if neither source provides a valid value.

// mote: The legacy value `' * '` was used historically to denote an unspecified
// document type. We want to ensure it's not used to initialize `documentType`.
const [documentType, setDocumentType] = useState<string | null>(() => {
// check for a valid `typeFromProps` (excluding the legacy value).
if (typeFromProps && typeFromProps !== '*') return typeFromProps

// determine the document type from the template's schema type.
const typeFromTemplates = templateName && templatesById[templateName]?.schemaType
if (typeFromTemplates) return typeFromTemplates

// default to `null` if no valid type is determined.
return null
})

useEffect(() => {
// exit early if document type is already determined.
if (documentType) return undefined

// fetch and set the document type from the document store
const subscription = documentStore
.resolveTypeForDocument(documentId)
// note: this operation is only done once to maintain consistency with
// other non-reactive code paths.
.pipe(first())
.subscribe({
next: (documentTypeFromContentLake) => {
if (documentTypeFromContentLake) {
setDocumentType(documentTypeFromContentLake)
} else {
setError(
new Error(`Could not resolve document type for document with ID ${documentId}`),
)
}
},
error: setError,
})

return () => subscription.unsubscribe()
}, [documentId, documentStore, documentType])

if (!documentType) return <>{fallback}</>

const contextValue: DocumentIdAndTypeContextValue = {
documentId,
documentType,
}

return (
<DocumentIdAndTypeContext.Provider value={contextValue}>
{children}
</DocumentIdAndTypeContext.Provider>
)
}
69 changes: 69 additions & 0 deletions packages/sanity/src/core/document/DocumentProvider.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import React from 'react'
import {noop} from 'lodash'
import {InitialValueProvider, InitialValueProviderProps} from './initialValue'
import {
DocumentIdAndTypeProvider,
DocumentIdAndTypeProviderProps,
} from './DocumentIdAndTypeProvider'
import {TimelineProvider, TimelineProviderProps} from './timeline'
import {FormStateProvider, FormStateProviderProps} from './formState'
import {
ReferenceInputOptionsProvider,
ReferenceInputOptionsProviderProps,
} from './referenceInputOptions'

/** @internal */
export interface DocumentProviderProps
extends DocumentIdAndTypeProviderProps,
TimelineProviderProps,
InitialValueProviderProps,
FormStateProviderProps,
ReferenceInputOptionsProviderProps {}

/** @internal */
export function DocumentProvider({
documentId,
documentType,
templateName,
templateParams,
timelineRange = {},
onTimelineRangeChange = noop,
initialFocusPath,
isHistoryInspectorOpen,
fallback,
children,
EditReferenceLinkComponent,
onEditReference,
activePath,
}: DocumentProviderProps) {
return (
<DocumentIdAndTypeProvider
documentId={documentId}
documentType={documentType}
templateName={templateName}
fallback={fallback}
>
<TimelineProvider timelineRange={timelineRange} onTimelineRangeChange={onTimelineRangeChange}>
<InitialValueProvider
templateName={templateName}
templateParams={templateParams}
fallback={fallback}
>
<ReferenceInputOptionsProvider
EditReferenceLinkComponent={EditReferenceLinkComponent}
onEditReference={onEditReference}
activePath={activePath}
fallback={fallback}
>
<FormStateProvider
initialFocusPath={initialFocusPath}
isHistoryInspectorOpen={isHistoryInspectorOpen}
>
{children}
</FormStateProvider>
</ReferenceInputOptionsProvider>
</InitialValueProvider>
</TimelineProvider>
</DocumentIdAndTypeProvider>
)
}
83 changes: 83 additions & 0 deletions packages/sanity/src/core/document/formState/FormStateContext.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import {SanityDocumentLike, ObjectSchemaType, ValidationMarker, Path} from '@sanity/types'
import {createContext} from 'react'
import {PatchEvent, DocumentFormNode, StateTree} from '../../form'
import {ConnectionState} from '../../hooks'
import {EditStateFor, PermissionCheckResult} from '../../store'

export interface FormStateContextValue<TDocument extends SanityDocumentLike> {
documentId: string
documentType: string

/**
* the current value of the form. note that this supersedes both the `value`
* and the `displayed` prop from the previous `DocumentPaneContextValue`
* interface
*/
value: TDocument

editState: EditStateFor

/**
* if the value originates from the current value from context lake, then this
* will be `current-value`. if the value is from a historical revision then
* it will be `historical-value`, if the value is from an initial value
* template then it will be `initial-value`
*/
valueOrigin: 'draft-value' | 'published-value' | 'initial-value' | 'historical-value' | undefined

schemaType: ObjectSchemaType

/**
* The value that is used to compare changes since a particular time. This is
* used to show change indicators. For example, this value is typically the
* published version of the document so that while you're editing the draft,
* the published version can be compared to the current draft version.
*/
compareValue: TDocument | null

/**
* Signals when the document is ready for editing. Considers the connection
* state, edit states, and whether or not the timeline is ready.
*/
ready: boolean

/**
* Propagates changes described by a patch event message to the form value.
*/
patchValue: (event: PatchEvent) => void

/**
* Contains the prepared root form node state. This is the result of
* `prepareFormState`.
*/
formState: DocumentFormNode | null

focusPath: Path
setFocusPath: (path: Path) => void

openPath: Path
setOpenPath: (path: Path) => void

collapsedFieldsets: StateTree<boolean>
setFieldsetCollapsed: (path: Path, collapsed: boolean) => void

collapsedPaths: StateTree<boolean>
setPathCollapsed: (path: Path, collapsed: boolean) => void

activeFieldGroups: StateTree<string>
setActiveFieldGroup: (path: Path, groupName: string) => void

validation: ValidationMarker[]
permissions: PermissionCheckResult | undefined
isPermissionsLoading: boolean

connectionState: ConnectionState

delete: () => void
isDeleting: boolean
isDeleted: boolean
}

export const FormStateContext = createContext<FormStateContextValue<SanityDocumentLike> | null>(
null,
)
Loading

0 comments on commit c5407b9

Please sign in to comment.