Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(titles): Update titles inside desk tool (#4887)
* feat(titles): Update titles inside desk tool When the user is moving through documents and panes, update the title property to be more specific to the current route * test(titles): Adds tests to Desktitle component * feat(titles): Adds controlsDocumentTitle property to Tool * fix(desk): try simplify title creation, always display the last document pane title where applicable (#4898) --------- Co-authored-by: Robin Pyon <robinpyon@users.noreply.github.com>
- Loading branch information
1 parent
b3b365e
commit 2ace4f1
Showing
7 changed files
with
285 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
170 changes: 170 additions & 0 deletions
170
packages/sanity/src/desk/components/deskTool/DeskTitle.test.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,170 @@ | ||
import React from 'react' | ||
import {render} from '@testing-library/react' | ||
import {Panes} from '../../structureResolvers' | ||
import * as USE_DESK_TOOL from '../../useDeskTool' | ||
import {DeskTitle} from './DeskTitle' | ||
import * as SANITY from 'sanity' | ||
|
||
jest.mock('sanity') | ||
|
||
describe('DeskTitle', () => { | ||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment | ||
// @ts-ignore it's a minimal mock implementation of useDeskTool | ||
jest.spyOn(USE_DESK_TOOL, 'useDeskTool').mockImplementation(() => ({ | ||
structureContext: {title: 'My Desk Tool'}, | ||
})) | ||
describe('Non document panes', () => { | ||
const mockPanes: Panes['resolvedPanes'] = [ | ||
{ | ||
id: 'content', | ||
type: 'list', | ||
title: 'Content', | ||
}, | ||
{ | ||
id: 'author', | ||
type: 'documentList', | ||
title: 'Author', | ||
schemaTypeName: 'author', | ||
options: { | ||
filter: '_type == $type', | ||
}, | ||
}, | ||
{ | ||
id: 'documentEditor', | ||
type: 'document', | ||
title: 'Authors created', | ||
options: { | ||
id: 'fake-document', | ||
type: 'author', | ||
}, | ||
}, | ||
] | ||
beforeEach(() => { | ||
document.title = 'Sanity Studio' | ||
}) | ||
it('renders the correct title when the content pane is open', () => { | ||
render(<DeskTitle resolvedPanes={mockPanes.slice(0, 1)} />) | ||
expect(document.title).toBe('Content | My Desk Tool') | ||
}) | ||
it('renders the correct title when an inner pane is open', () => { | ||
render(<DeskTitle resolvedPanes={mockPanes.slice(0, 2)} />) | ||
expect(document.title).toBe('Author | My Desk Tool') | ||
}) | ||
it('renders the correct title when the document pane has a title', () => { | ||
render(<DeskTitle resolvedPanes={mockPanes} />) | ||
expect(document.title).toBe('Authors created | My Desk Tool') | ||
}) | ||
it('should not update the title if no panes are available', () => { | ||
render(<DeskTitle resolvedPanes={[]} />) | ||
expect(document.title).toBe('Sanity Studio') | ||
}) | ||
}) | ||
describe('With document panes', () => { | ||
const mockPanes: Panes['resolvedPanes'] = [ | ||
{ | ||
id: 'content', | ||
type: 'list', | ||
title: 'Content', | ||
}, | ||
{ | ||
id: 'author', | ||
type: 'documentList', | ||
title: 'Author', | ||
schemaTypeName: 'author', | ||
options: { | ||
filter: '_type == $type', | ||
}, | ||
}, | ||
{ | ||
id: 'documentEditor', | ||
type: 'document', | ||
title: '', | ||
options: { | ||
id: 'fake-document', | ||
type: 'author', | ||
}, | ||
}, | ||
] | ||
|
||
const doc = { | ||
name: 'Foo', | ||
_id: 'drafts.fake-document', | ||
_type: 'author', | ||
_updatedAt: '', | ||
_createdAt: '', | ||
_rev: '', | ||
} | ||
const editState = { | ||
ready: true, | ||
type: 'author', | ||
draft: doc, | ||
published: null, | ||
id: 'fake-document', | ||
transactionSyncLock: {enabled: false}, | ||
liveEdit: false, | ||
} | ||
const valuePreview = { | ||
isLoading: false, | ||
value: { | ||
title: doc.name, | ||
}, | ||
} | ||
const useSchemaMock = () => | ||
({ | ||
get: () => ({ | ||
title: 'Author', | ||
name: 'author', | ||
type: 'document', | ||
}), | ||
}) as unknown as SANITY.Schema | ||
|
||
it('should not update the when the document is still loading', () => { | ||
const useEditStateMock = () => ({...editState, ready: false}) | ||
const useValuePreviewMock = () => valuePreview | ||
jest.spyOn(SANITY, 'useSchema').mockImplementationOnce(useSchemaMock) | ||
jest.spyOn(SANITY, 'useEditState').mockImplementationOnce(useEditStateMock) | ||
jest.spyOn(SANITY, 'unstable_useValuePreview').mockImplementationOnce(useValuePreviewMock) | ||
|
||
document.title = 'Sanity Studio' | ||
render(<DeskTitle resolvedPanes={mockPanes} />) | ||
expect(document.title).toBe('Sanity Studio') | ||
}) | ||
|
||
it('renders the correct title when the document pane has a title', () => { | ||
const useEditStateMock = () => editState | ||
const useValuePreviewMock = () => valuePreview | ||
jest.spyOn(SANITY, 'useSchema').mockImplementationOnce(useSchemaMock) | ||
jest.spyOn(SANITY, 'useEditState').mockImplementationOnce(useEditStateMock) | ||
jest.spyOn(SANITY, 'unstable_useValuePreview').mockImplementationOnce(useValuePreviewMock) | ||
|
||
document.title = 'Sanity Studio' | ||
render(<DeskTitle resolvedPanes={mockPanes} />) | ||
expect(document.title).toBe('Foo | My Desk Tool') | ||
}) | ||
it('renders the correct title when the document is new', () => { | ||
const useEditStateMock = () => ({...editState, draft: null}) | ||
const useValuePreviewMock = () => valuePreview | ||
jest.spyOn(SANITY, 'useSchema').mockImplementationOnce(useSchemaMock) | ||
jest.spyOn(SANITY, 'useEditState').mockImplementationOnce(useEditStateMock) | ||
jest.spyOn(SANITY, 'unstable_useValuePreview').mockImplementationOnce(useValuePreviewMock) | ||
|
||
document.title = 'Sanity Studio' | ||
render(<DeskTitle resolvedPanes={mockPanes} />) | ||
expect(document.title).toBe('New Author | My Desk Tool') | ||
}) | ||
it('renders the correct title when the document is untitled', () => { | ||
const useEditStateMock = () => editState | ||
const useValuePreviewMock = () => ({ | ||
isLoading: false, | ||
value: {title: ''}, | ||
}) | ||
jest.spyOn(SANITY, 'useSchema').mockImplementationOnce(useSchemaMock) | ||
jest.spyOn(SANITY, 'useEditState').mockImplementationOnce(useEditStateMock) | ||
jest.spyOn(SANITY, 'unstable_useValuePreview').mockImplementationOnce(useValuePreviewMock) | ||
|
||
document.title = 'Sanity Studio' | ||
render(<DeskTitle resolvedPanes={mockPanes} />) | ||
expect(document.title).toBe('Untitled | My Desk Tool') | ||
}) | ||
}) | ||
}) |
99 changes: 99 additions & 0 deletions
99
packages/sanity/src/desk/components/deskTool/DeskTitle.tsx
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,99 @@ | ||
import React, {useEffect} from 'react' | ||
import {ObjectSchemaType} from '@sanity/types' | ||
import {Panes} from '../../structureResolvers' | ||
import {useDeskTool} from '../../useDeskTool' | ||
import {LOADING_PANE} from '../../constants' | ||
import {DocumentPaneNode} from '../../types' | ||
import {useEditState, useSchema, unstable_useValuePreview as useValuePreview} from 'sanity' | ||
|
||
interface DeskTitleProps { | ||
resolvedPanes: Panes['resolvedPanes'] | ||
} | ||
|
||
const DocumentTitle = (props: {documentId: string; documentType: string}) => { | ||
const {documentId, documentType} = props | ||
const editState = useEditState(documentId, documentType) | ||
const schema = useSchema() | ||
const isNewDocument = !editState?.published && !editState?.draft | ||
const documentValue = editState?.draft || editState?.published | ||
const schemaType = schema.get(documentType) as ObjectSchemaType | undefined | ||
|
||
const {value, isLoading: previewValueIsLoading} = useValuePreview({ | ||
enabled: true, | ||
schemaType, | ||
value: documentValue, | ||
}) | ||
|
||
const documentTitle = isNewDocument | ||
? `New ${schemaType?.title || schemaType?.name}` | ||
: value?.title || 'Untitled' | ||
|
||
const settled = editState.ready && !previewValueIsLoading | ||
const newTitle = useConstructDocumentTitle(documentTitle) | ||
useEffect(() => { | ||
if (!settled) return | ||
// Set the title as the document title | ||
document.title = newTitle | ||
}, [documentTitle, settled, newTitle]) | ||
|
||
return null | ||
} | ||
|
||
const PassthroughTitle = (props: {title?: string}) => { | ||
const {title} = props | ||
const newTitle = useConstructDocumentTitle(title) | ||
useEffect(() => { | ||
// Set the title as the document title | ||
document.title = newTitle | ||
}, [newTitle, title]) | ||
return null | ||
} | ||
|
||
export const DeskTitle = (props: DeskTitleProps) => { | ||
const {resolvedPanes} = props | ||
|
||
if (!resolvedPanes?.length) return null | ||
|
||
const lastPane = resolvedPanes[resolvedPanes.length - 1] | ||
|
||
// If the last pane is loading, display the desk tool title only | ||
if (isLoadingPane(lastPane)) { | ||
return <PassthroughTitle /> | ||
} | ||
|
||
// If the last pane is a document | ||
if (isDocumentPane(lastPane)) { | ||
// Passthrough the document pane's title, which may be defined in structure builder | ||
if (lastPane?.title) { | ||
return <PassthroughTitle title={lastPane.title} /> | ||
} | ||
|
||
// Otherwise, display a `document.title` containing the resolved Sanity document title | ||
return <DocumentTitle documentId={lastPane.options.id} documentType={lastPane.options.type} /> | ||
} | ||
|
||
// Otherwise, display the last pane's title (if present) | ||
return <PassthroughTitle title={lastPane?.title} /> | ||
} | ||
|
||
/** | ||
* Construct a pipe delimited title containing `activeTitle` (if applicable) and the base desk title. | ||
* | ||
* @param activeTitle - Title of the first segment | ||
* | ||
* @returns A pipe delimited title in the format `${activeTitle} | %BASE_DESK_TITLE%` | ||
* or simply `%BASE_DESK_TITLE` if `activeTitle` is undefined. | ||
*/ | ||
function useConstructDocumentTitle(activeTitle?: string) { | ||
const deskToolBaseTitle = useDeskTool().structureContext.title | ||
return [activeTitle, deskToolBaseTitle].filter((title) => title).join(' | ') | ||
} | ||
|
||
// Type guards | ||
function isDocumentPane(pane: Panes['resolvedPanes'][number]): pane is DocumentPaneNode { | ||
return pane !== LOADING_PANE && pane.type === 'document' | ||
} | ||
|
||
function isLoadingPane(pane: Panes['resolvedPanes'][number]): pane is typeof LOADING_PANE { | ||
return pane === LOADING_PANE | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
2ace4f1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
performance-studio – ./
performance-studio-git-next.sanity.build
performance-studio.sanity.build
2ace4f1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Successfully deployed to the following URLs:
test-studio – ./
test-studio.sanity.build
test-studio-git-next.sanity.build