Skip to content

Commit

Permalink
fix: allow encoding draft ids (#982)
Browse files Browse the repository at this point in the history
* fix: allow encoding draft ids

* chore: remove unused import

* test(core-loader): revert ids, add `isDraft`
  • Loading branch information
rdunk committed Feb 27, 2024
1 parent 190b00b commit 637a33d
Show file tree
Hide file tree
Showing 13 changed files with 139 additions and 54 deletions.
11 changes: 7 additions & 4 deletions packages/core-loader/src/live-mode/enableLiveMode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,10 +229,13 @@ export function enableLiveMode(options: LazyEnableLiveModeOptions): () => void {
const key = JSON.stringify({ perspective, query, params })
const value = cache.get(key)
if (value) {
$fetch.setKey('data', value.result)
$fetch.setKey('sourceMap', value.resultSourceMap)
$fetch.setKey('perspective', perspective)
$fetch.setKey('loading', false)
$fetch.set({
data: value.result,
error: undefined,
loading: false,
perspective,
sourceMap: value.resultSourceMap,
})
documentsOnPage.push(...(value.resultSourceMap?.documents ?? []))
}
}
Expand Down
10 changes: 5 additions & 5 deletions packages/core-loader/test/encodeDataAttribute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -714,14 +714,14 @@ describe('encodeDataAttribute', () => {

test('string paths', () => {
expect(encodeDataAttribute('page.sections[4].style')).toMatchInlineSnapshot(
`"id=home;type=page;path=sections:0bd049fc047a.style;base=%2Fstudio"`,
`"id=home;type=page;path=sections:0bd049fc047a.style;base=%2Fstudio;isDraft"`,
)
})
test('array paths', () => {
expect(
encodeDataAttribute(['page', 'sections', 4, 'style']),
).toMatchInlineSnapshot(
`"id=home;type=page;path=sections:0bd049fc047a.style;base=%2Fstudio"`,
`"id=home;type=page;path=sections:0bd049fc047a.style;base=%2Fstudio;isDraft"`,
)
})
})
Expand All @@ -735,20 +735,20 @@ describe('scoping', () => {
test('string paths', () => {
const scoped = encodeDataAttribute.scope('page.sections[4]')
expect(scoped('style')).toMatchInlineSnapshot(
`"id=home;type=page;path=sections:0bd049fc047a.style;base=%2Fstudio"`,
`"id=home;type=page;path=sections:0bd049fc047a.style;base=%2Fstudio;isDraft"`,
)
})
test('array paths', () => {
const scoped = encodeDataAttribute.scope(['page', 'sections', 4])
expect(scoped(['style'])).toMatchInlineSnapshot(
`"id=home;type=page;path=sections:0bd049fc047a.style;base=%2Fstudio"`,
`"id=home;type=page;path=sections:0bd049fc047a.style;base=%2Fstudio;isDraft"`,
)
})
test('array paths recursive', () => {
const scoped = encodeDataAttribute.scope(['page'])
const scopedDeeper = scoped.scope(['sections', 4])
expect(scopedDeeper(['style'])).toMatchInlineSnapshot(
`"id=home;type=page;path=sections:0bd049fc047a.style;base=%2Fstudio"`,
`"id=home;type=page;path=sections:0bd049fc047a.style;base=%2Fstudio;isDraft"`,
)
})
})
8 changes: 8 additions & 0 deletions packages/presentation/src/PresentationTool.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,14 @@ export default function PresentationTool(props: {
[navigate],
)

// Dispatch a perspective message when the perspective changes
useEffect(() => {
channel?.send('overlays', 'presentation/perspective', {
perspective: state.perspective,
})
}, [channel, state.perspective])

// Dispatch a focus or blur message when the id or path change
useEffect(() => {
if (params.id && params.path) {
channel?.send('overlays', 'presentation/focus', {
Expand Down
2 changes: 2 additions & 0 deletions packages/presentation/src/loader/LoaderQueries.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ export default function LoaderQueries(props: LoaderQueriesProps): JSX.Element {
useEffect(() => {
if (channel) {
const { projectId, dataset } = clientConfig
// @todo - Can this be migrated/deprecated in favour of emitting

Check warning on line 56 in packages/presentation/src/loader/LoaderQueries.tsx

View workflow job for this annotation

GitHub Actions / Are there issues that linters can fix? 🤔

Unexpected '@todo' comment: '@todo - Can this be migrated/deprecated...'

Check warning on line 56 in packages/presentation/src/loader/LoaderQueries.tsx

View workflow job for this annotation

GitHub Actions / build

Unexpected '@todo' comment: '@todo - Can this be migrated/deprecated...'
// `presentation/perspective` at a higher level?
channel.send('loaders', 'loader/perspective', {
projectId: projectId!,
dataset: dataset!,
Expand Down
2 changes: 1 addition & 1 deletion packages/presentation/src/useDocumentsOnPage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export function useDocumentsOnPage(
(
key: string,
perspective: ClientPerspective,
sourceDocuments: DocumentOnPage[],
sourceDocuments: DocumentOnPage[] = [],
) => {
const documents = sourceDocuments.filter((sourceDocument) => {
if ('_projectId' in sourceDocument && sourceDocument._projectId) {
Expand Down
4 changes: 4 additions & 0 deletions packages/presentation/src/useParams.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
PresentationSearchParams,
PresentationStateParams,
} from './types'
import { getPublishedId } from 'sanity'

function pruneObject<T extends RouterState | PresentationParams>(obj: T): T {
return Object.fromEntries(
Expand Down Expand Up @@ -104,6 +105,9 @@ export function useParams({
() =>
debounce<PresentationNavigate>(
(nextState, nextSearchState = {}, forceReplace) => {
// Force navigation to use published IDs only
if (nextState.id) nextState.id = getPublishedId(nextState.id)

// Extract type, id and path as 'routerState'
const { _searchParams: routerSearchParams, ...routerState } =
routerStateRef.current
Expand Down
20 changes: 14 additions & 6 deletions packages/svelte-loader/src/defineUseQuery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,12 +45,20 @@ export function defineUseQuery({
onMount(() =>
$fetcher.subscribe((snapshot) => {
const prev = get($writeable)
if (
prev.error !== snapshot.error ||
prev.loading !== snapshot.loading ||
prev.perspective !== snapshot.perspective ||
!isEqual(prev.data, snapshot.data)
) {

if (prev.error !== snapshot.error) {
$writeable.set(snapshot)
}

if (prev.loading !== snapshot.loading) {
$writeable.set(snapshot)
}

if (prev.perspective !== snapshot.perspective) {
$writeable.set(snapshot)
}

if (!isEqual(prev.data, snapshot.data)) {
$writeable.set(snapshot)
}
}),
Expand Down
16 changes: 14 additions & 2 deletions packages/visual-editing-helpers/src/csm/transformSanityNodeData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import { urlStringToPath } from '../urlStringToPath'

export type { SanityNode, SanityStegaNode }

export const DRAFTS_PREFIX = 'drafts.'

const lengthyStr = string([minLength(1)])
const optionalLengthyStr = optional(lengthyStr)

Expand All @@ -29,6 +31,7 @@ const sanityNodeSchema = object({
tool: optionalLengthyStr,
type: optionalLengthyStr,
workspace: optionalLengthyStr,
isDraft: optional(string()),
})

const sanityLegacyNodeSchema = object({
Expand Down Expand Up @@ -71,11 +74,17 @@ export function encodeSanityNodeData(node: SanityNode): string | undefined {
['base', encodeURIComponent(baseUrl)],
['workspace', workspace],
['tool', tool],
['isDraft', _id.startsWith(DRAFTS_PREFIX)],
]

return parts
.filter(([, value]) => !!value)
.map((part) => part.join('='))
.map((part) => {
const [key, value] = part
// For true values, just display the key
if (value === true) return key
return part.join('=')
})
.join(';')
}

Expand All @@ -89,7 +98,7 @@ export function decodeSanityString(str: string): SanityNode | undefined {

const data = segments.reduce((acc, segment) => {
const [key, value] = segment.split('=')
if (!key || !value) return acc
if (!key || (segment.includes('=') && !value)) return acc

switch (key) {
case 'id':
Expand All @@ -116,6 +125,9 @@ export function decodeSanityString(str: string): SanityNode | undefined {
case 'dataset':
acc.dataset = value
break
case 'isDraft':
acc.isDraft = ''
break
default:
}

Expand Down
13 changes: 10 additions & 3 deletions packages/visual-editing-helpers/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,14 @@ export type QueryCacheKey = `${string}-${string}`
* @public
*/
export type SanityNode = {
projectId?: string
baseUrl: string
dataset?: string
id: string
isDraft?: string
path: string
type?: string
baseUrl: string
projectId?: string
tool?: string
type?: string
workspace?: string
}

Expand Down Expand Up @@ -121,6 +122,12 @@ export type PresentationMsg =
type: 'presentation/refresh'
data: HistoryRefresh
}
| {
type: 'presentation/perspective'
data: {
perspective: ClientPerspective
}
}

/**@public */
export interface VisualEditingPayloads {
Expand Down
13 changes: 6 additions & 7 deletions packages/visual-editing/src/controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,15 +266,14 @@ export function createOverlayController({
}

if (node instanceof HTMLElement) {
// @todo - We need to handle cases where `data-sanity` attributes may

Check warning on line 269 in packages/visual-editing/src/controller.ts

View workflow job for this annotation

GitHub Actions / Are there issues that linters can fix? 🤔

Unexpected '@todo' comment: '@todo - We need to handle cases where...'
// have changed, so it's not enough to ignore previously registered
// elements. We can just unregister and re-register elements instead of
// attempting to update their data. Can this be made more efficient?
if (elementsMap.has(node)) {
const sanityNodes = findSanityNodes({ childNodes: [node] })
// Check existing nodes are still valid
if (!sanityNodes.length) {
unregisterElement(node)
}
} else {
registerElements({ childNodes: [node] })
unregisterElement(node)
}
registerElements({ childNodes: [node] })
}

return true
Expand Down
71 changes: 48 additions & 23 deletions packages/visual-editing/src/ui/Overlays.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import type { ChannelStatus } from '@sanity/channels'
import type { ContentSourceMapDocuments } from '@sanity/client'
import type {
ClientPerspective,
ContentSourceMapDocuments,
} from '@sanity/client'
import {
isHTMLAnchorElement,
isHTMLElement,
Expand All @@ -11,6 +14,7 @@ import {
isHotkey,
type SanityNode,
} from '@sanity/visual-editing-helpers'
import { DRAFTS_PREFIX } from '@sanity/visual-editing-helpers/csm'
import {
type FunctionComponent,
useCallback,
Expand Down Expand Up @@ -77,14 +81,14 @@ export const Overlays: FunctionComponent<{

const [status, setStatus] = useState<ChannelStatus>()

const [{ elements, wasMaybeCollapsed }, dispatch] = useReducer(
const [{ elements, wasMaybeCollapsed, perspective }, dispatch] = useReducer(
overlayStateReducer,
undefined,
() => ({
{
elements: [],
focusPath: '',
wasMaybeCollapsed: false,
}),
perspective: 'published',
},
)
const [rootElement, setRootElement] = useState<HTMLElement | null>(null)
const [overlayEnabled, setOverlayEnabled] = useState(true)
Expand All @@ -96,6 +100,8 @@ export const Overlays: FunctionComponent<{
dispatch({ type, data })
} else if (type === 'presentation/blur') {
dispatch({ type, data })
} else if (type === 'presentation/perspective') {
dispatch({ type, data })
} else if (type === 'presentation/navigate') {
history?.update(data)
} else if (type === 'presentation/toggleOverlay') {
Expand All @@ -109,49 +115,68 @@ export const Overlays: FunctionComponent<{
}
}, [channel, history])

const nodeIdsRef = useRef<Set<string>>(new Set())
const lastReported = useRef<
| {
nodeIds: Set<string>
perspective: ClientPerspective
}
| undefined
>(undefined)

const reportDocuments = useCallback(
(documents: ContentSourceMapDocuments) => {
(documents: ContentSourceMapDocuments, perspective: ClientPerspective) => {
channel?.send('visual-editing/documents', {
perspective: 'previewDrafts',
documents,
perspective,
})
},
[channel],
)

useEffect(() => {
// Report only `SanityNode`, if a node is untransformed (`SanityStegaNode`)
// at this stage it will not contain the necessary document data
// Report only nodes of type `SanityNode`. Untransformed `SanityStegaNode`
// nodes without an `id`, are not reported as they will not contain the
// necessary document data.
const nodes = elements
.map((e) => e.sanity)
.filter((s) => 'id' in s) as SanityNode[]
.map((e) => {
const { sanity } = e
if (!('id' in sanity)) return null
return {
...sanity,
id: 'isDraft' in sanity ? `${DRAFTS_PREFIX}${sanity.id}` : sanity.id,
}
})
.filter((s) => !!s) as SanityNode[]

const nodeIds = new Set<string>(nodes.map((e) => e.id))
// Only report documents if some document IDs have changed
if (!isEqualSets(nodeIds, nodeIdsRef.current)) {
// Report if:
// - Documents not yet reported
// - Document IDs changed
// - Perspective changed
if (
!lastReported.current ||
!isEqualSets(nodeIds, lastReported.current.nodeIds) ||
perspective !== lastReported.current.perspective
) {
const documentsOnPage: ContentSourceMapDocuments = Array.from(
nodeIds,
).map((_id) => {
const {
type,
projectId: _projectId,
dataset: _dataset,
} = nodes.find((node) => node.id === _id)!
const node = nodes.find((node) => node.id === _id)!
const { type, projectId: _projectId, dataset: _dataset } = node
return _projectId && _dataset
? { _id, _type: type!, _projectId, _dataset }
: { _id, _type: type! }
})
nodeIdsRef.current = nodeIds
reportDocuments(documentsOnPage)
lastReported.current = { nodeIds, perspective }
reportDocuments(documentsOnPage, perspective)
}
}, [elements, reportDocuments])
}, [elements, perspective, reportDocuments])

const overlayEventHandler: OverlayEventHandler = useCallback(
(message) => {
if (message.type === 'element/click') {
channel.send('overlay/focus', message.sanity)
const { sanity } = message
channel.send('overlay/focus', sanity)
} else if (message.type === 'overlay/activate') {
channel.send('overlay/toggle', { enabled: true })
} else if (message.type === 'overlay/deactivate') {
Expand Down
Loading

0 comments on commit 637a33d

Please sign in to comment.