Skip to content

Commit

Permalink
feat(vision): add release layering support
Browse files Browse the repository at this point in the history
  • Loading branch information
juice49 committed Jan 20, 2025
1 parent 62a5563 commit 17d371e
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 31 deletions.
3 changes: 2 additions & 1 deletion packages/@sanity/vision/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,8 @@
"json5": "^2.2.3",
"lodash": "^4.17.21",
"quick-lru": "^5.1.1",
"react-compiler-runtime": "19.0.0-beta-55955c9-20241229"
"react-compiler-runtime": "19.0.0-beta-55955c9-20241229",
"react-fast-compare": "^3.2.2"
},
"devDependencies": {
"@repo/package.config": "workspace:*",
Expand Down
5 changes: 3 additions & 2 deletions packages/@sanity/vision/src/SanityVision.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {type Tool, useClient} from 'sanity'
import {type Tool, useClient, usePerspective} from 'sanity'

import {DEFAULT_API_VERSION} from './apiVersions'
import {VisionContainer} from './containers/VisionContainer'
Expand All @@ -11,14 +11,15 @@ interface SanityVisionProps {

function SanityVision(props: SanityVisionProps) {
const client = useClient({apiVersion: '1'})
const perspective = usePerspective()
const config: VisionConfig = {
defaultApiVersion: DEFAULT_API_VERSION,
...props.tool.options,
}

return (
<VisionErrorBoundary>
<VisionContainer client={client} config={config} />
<VisionContainer client={client} config={config} pinnedPerspective={perspective} />
</VisionErrorBoundary>
)
}
Expand Down
186 changes: 164 additions & 22 deletions packages/@sanity/vision/src/components/VisionGui.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
/* eslint-disable complexity */
import {SplitPane} from '@rexxars/react-split-pane'
import {type ListenEvent, type MutationEvent, type SanityClient} from '@sanity/client'
import {
type ClientPerspective,
type ListenEvent,
type MutationEvent,
type SanityClient,
} from '@sanity/client'
import {CopyIcon, ErrorOutlineIcon, PlayIcon, StopIcon} from '@sanity/icons'
import {
Box,
Expand All @@ -19,14 +24,23 @@ import {
} from '@sanity/ui'
import {isHotkey} from 'is-hotkey-esm'
import {debounce} from 'lodash'
import {type ChangeEvent, createRef, PureComponent, type RefObject} from 'react'
import {type TFunction, Translate} from 'sanity'
import {
type ChangeEvent,
type ComponentType,
createRef,
PureComponent,
type RefObject,
useMemo,
} from 'react'
import isEqual from 'react-fast-compare'
import {type PerspectiveValue, type TFunction, Translate} from 'sanity'

import {API_VERSIONS, DEFAULT_API_VERSION} from '../apiVersions'
import {VisionCodeMirror} from '../codemirror/VisionCodeMirror'
import {
DEFAULT_PERSPECTIVE,
isSupportedPerspective,
isVirtualPerspective,
SUPPORTED_PERSPECTIVES,
type SupportedPerspective,
} from '../perspectives'
Expand Down Expand Up @@ -192,6 +206,14 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
perspective = DEFAULT_PERSPECTIVE
}

if (perspective == 'pinnedRelease' && !hasPinnedPerspective(this.props.pinnedPerspective)) {
perspective = DEFAULT_PERSPECTIVE
}

if (perspective !== 'pinnedRelease' && hasPinnedPerspective(this.props.pinnedPerspective)) {
perspective = 'pinnedRelease'
}

if (typeof lastQuery !== 'string') {
lastQuery = ''
}
Expand All @@ -209,7 +231,10 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
this._client = props.client.withConfig({
apiVersion: customApiVersion || apiVersion,
dataset,
perspective: perspective,
perspective: getActivePerspective({
visionPerspective: perspective,
pinnedPerspective: this.props.pinnedPerspective,
}),
allowReconfigure: true,
})

Expand Down Expand Up @@ -264,6 +289,7 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
this.handleKeyDown = this.handleKeyDown.bind(this)
this.handleResize = this.handleResize.bind(this)
this.handleOnPasteCapture = this.handleOnPasteCapture.bind(this)
this.setPerspective = this.setPerspective.bind(this)
}

componentDidMount() {
Expand All @@ -280,6 +306,30 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
this.cancelResizeListener()
}

componentDidUpdate(prevProps: Readonly<VisionGuiProps>): void {
if (hasPinnedPerspectiveChanged(prevProps.pinnedPerspective, this.props.pinnedPerspective)) {
if (
this.state.perspective !== 'pinnedRelease' &&
hasPinnedPerspective(this.props.pinnedPerspective)
) {
this.setPerspective('pinnedRelease')
return
}

if (
this.state.perspective === 'pinnedRelease' &&
!hasPinnedPerspective(this.props.pinnedPerspective)
) {
this.setPerspective('raw')
return
}

if (this.state.perspective === 'pinnedRelease') {
this.setPerspective('pinnedRelease')
}
}
}

handleResizeListen() {
if (!this._visionRoot.current) {
return
Expand Down Expand Up @@ -338,11 +388,17 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
}
}

const perspective = isSupportedPerspective(parts.options.perspective)
? parts.options.perspective
: undefined

if (perspective && !isSupportedPerspective(perspective)) {
const perspective =
isSupportedPerspective(parts.options.perspective) &&
!isVirtualPerspective(parts.options.perspective)
? parts.options.perspective
: undefined

if (
perspective &&
(!isSupportedPerspective(parts.options.perspective) ||
isVirtualPerspective(parts.options.perspective))
) {
this.props.toast.push({
closable: true,
id: 'vision-paste-unsupported-perspective',
Expand Down Expand Up @@ -378,7 +434,10 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
this._client.config({
dataset: this.state.dataset,
apiVersion: customApiVersion || apiVersion,
perspective: this.state.perspective,
perspective: getActivePerspective({
visionPerspective: this.state.perspective,
pinnedPerspective: this.props.pinnedPerspective,
}),
})
this.handleQueryExecution()
this.props.toast.push({
Expand All @@ -399,7 +458,6 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
if (!this._querySubscription) {
return
}

this._querySubscription.unsubscribe()
this._querySubscription = undefined
}
Expand Down Expand Up @@ -466,14 +524,21 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {

handleChangePerspective(evt: ChangeEvent<HTMLSelectElement>) {
const perspective = evt.target.value
this.setPerspective(perspective)
}

setPerspective(perspective: string): void {
if (!isSupportedPerspective(perspective)) {
return
}

this.setState({perspective}, () => {
this._localStorage.set('perspective', this.state.perspective)
this._client.config({
perspective: this.state.perspective,
perspective: getActivePerspective({
visionPerspective: this.state.perspective,
pinnedPerspective: this.props.pinnedPerspective,
}),
})
this.handleQueryExecution()
})
Expand Down Expand Up @@ -598,9 +663,13 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {

this.ensureSelectedApiVersion()

const urlQueryOpts: Record<string, string> = {}
const urlQueryOpts: Record<string, string | string[]> = {}
if (this.state.perspective !== 'raw') {
urlQueryOpts.perspective = this.state.perspective
urlQueryOpts.perspective =
getActivePerspective({
visionPerspective: this.state.perspective,
pinnedPerspective: this.props.pinnedPerspective,
}) ?? []
}

const url = this._client.getUrl(
Expand All @@ -613,20 +682,22 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
this._querySubscription = this._client.observable
.fetch(query, params, {filterResponse: false, tag: 'vision'})
.subscribe({
next: (res) =>
next: (res) => {
this.setState({
queryTime: res.ms,
e2eTime: Date.now() - queryStart,
queryResult: res.result,
queryInProgress: false,
error: undefined,
}),
error: (error) =>
})
},
error: (error) => {
this.setState({
error,
query,
queryInProgress: false,
}),
})
},
})

return true
Expand Down Expand Up @@ -670,7 +741,7 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
}

render() {
const {datasets, t} = this.props
const {datasets, t, pinnedPerspective} = this.props
const {
apiVersion,
customApiVersion,
Expand Down Expand Up @@ -778,9 +849,21 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
</Card>

<Select value={perspective} onChange={this.handleChangePerspective}>
{SUPPORTED_PERSPECTIVES.map((p) => (
<option key={p}>{p}</option>
))}
{SUPPORTED_PERSPECTIVES.map((perspectiveName) => {
if (perspectiveName === 'pinnedRelease') {
return (
<>
<PinnedReleasePerspectiveOption
key="pinnedRelease"
pinnedPerspective={pinnedPerspective}
t={t}
/>
<hr />
</>
)
}
return <option key={perspectiveName}>{perspectiveName}</option>
})}
</Select>
</Stack>
</Box>
Expand Down Expand Up @@ -1027,3 +1110,62 @@ export class VisionGui extends PureComponent<VisionGuiProps, VisionGuiState> {
)
}
}

function getActivePerspective({
visionPerspective,
pinnedPerspective,
}: {
visionPerspective: ClientPerspective | SupportedPerspective
pinnedPerspective: PerspectiveValue
}): ClientPerspective | undefined {
if (visionPerspective !== 'pinnedRelease') {
return visionPerspective
}

if (pinnedPerspective.perspectiveStack.length !== 0) {
return pinnedPerspective.perspectiveStack
}

if (typeof pinnedPerspective.selectedPerspectiveName !== 'undefined') {
return [pinnedPerspective.selectedPerspectiveName]
}

return undefined
}

const PinnedReleasePerspectiveOption: ComponentType<{
pinnedPerspective: PerspectiveValue
t: TFunction
}> = ({pinnedPerspective, t}) => {
const name =
typeof pinnedPerspective.selectedPerspective === 'object'
? pinnedPerspective.selectedPerspective.metadata.title
: pinnedPerspective.selectedPerspectiveName

const label = hasPinnedPerspective(pinnedPerspective)
? `(${t('settings.perspectives.pinned-release-label')})`
: t('settings.perspectives.pinned-release-label')

const text = useMemo(
() => [name, label].filter((value) => typeof value !== 'undefined').join(' '),
[label, name],
)

return (
<option value="pinnedRelease" disabled={!hasPinnedPerspective(pinnedPerspective)}>
{text}
</option>
)
}

function hasPinnedPerspective({selectedPerspectiveName}: PerspectiveValue): boolean {
return typeof selectedPerspectiveName !== 'undefined'
}

function hasPinnedPerspectiveChanged(previous: PerspectiveValue, next: PerspectiveValue): boolean {
const hasPerspectiveStackChanged = !isEqual(previous.perspectiveStack, next.perspectiveStack)

return (
previous.selectedPerspectiveName !== next.selectedPerspectiveName || hasPerspectiveStackChanged
)
}
2 changes: 2 additions & 0 deletions packages/@sanity/vision/src/i18n/resources.ts
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,8 @@ const visionLocaleStrings = defineLocalesResources('vision', {
/** Description for popover that explains what "Perspectives" are */
'settings.perspectives.description':
'Perspectives allow your query to run against different "views" of the content in your dataset',
/** Label for the pinned release perspective */
'settings.perspectives.pinned-release-label': 'pinned release',
/** Title for popover that explains what "Perspectives" are */
'settings.perspectives.title': 'Perspectives',
} as const)
Expand Down
33 changes: 27 additions & 6 deletions packages/@sanity/vision/src/perspectives.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,36 @@
import {type ClientPerspective} from '@sanity/client'

export type SupportedPerspective = 'raw' | 'previewDrafts' | 'published' | 'drafts'

export const SUPPORTED_PERSPECTIVES = [
'pinnedRelease',
'raw',
'previewDrafts',
'published',
'drafts',
] satisfies ClientPerspective[]
export const DEFAULT_PERSPECTIVE = SUPPORTED_PERSPECTIVES[0]
] as const

export type SupportedPerspective = (typeof SUPPORTED_PERSPECTIVES)[number]

/**
* Virtual perspectives are recognised by Vision, but do not concretely reflect the names of real
* perspectives. Virtual perspectives are transformed into real perspectives before being used to
* interact with data.
*
* For example, the `pinnedRelease` virtual perspective is transformed to the real perspective
* currently pinned in Studio.
*/
export const VIRTUAL_PERSPECTIVES = ['pinnedRelease'] as const

export type VirtualPerspective = (typeof VIRTUAL_PERSPECTIVES)[number]

export const DEFAULT_PERSPECTIVE: SupportedPerspective = 'raw'

export function isSupportedPerspective(p: string): p is SupportedPerspective {
return SUPPORTED_PERSPECTIVES.includes(p as SupportedPerspective)
}

export function isVirtualPerspective(
maybeVirtualPerspective: unknown,
): maybeVirtualPerspective is VirtualPerspective {
return (
typeof maybeVirtualPerspective === 'string' &&
VIRTUAL_PERSPECTIVES.includes(maybeVirtualPerspective as VirtualPerspective)
)
}
Loading

0 comments on commit 17d371e

Please sign in to comment.