Skip to content

Commit

Permalink
feat(core, structure) sheet list menu option (#6593)
Browse files Browse the repository at this point in the history
* feat(structure): Rendering sheet list layout option in test-studio

* feat(structure): branching the rendering of different document list panes

* feat(structure): renaming of generic pane components

* feat(structure): fixing typing for new sheetList

* feat(structure): resolving a default export to named

* feat(structure): testing sheet view pane display logcic

* fix(structure): resolving testing for useStructureTool
  • Loading branch information
jordanl17 committed May 10, 2024
1 parent 80b4715 commit b8f9987
Show file tree
Hide file tree
Showing 9 changed files with 342 additions and 180 deletions.
53 changes: 36 additions & 17 deletions dev/test-studio/structure/resolveStructure.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
RocketIcon,
SyncIcon,
TerminalIcon,
UlistIcon,
UsersIcon,
} from '@sanity/icons'
import {uuid} from '@sanity/uuid'
Expand Down Expand Up @@ -415,23 +416,41 @@ export const structure: StructureResolver = (S, {schema, documentStore, i18n}) =

S.divider(),

...S.documentTypeListItems().filter((listItem) => {
const id = listItem.getId()

return (
id &&
!CI_INPUT_TYPES.includes(id) &&
!DEBUG_INPUT_TYPES.includes(id) &&
!STANDARD_INPUT_TYPES.includes(id) &&
!STANDARD_PORTABLE_TEXT_INPUT_TYPES.includes(id) &&
!PLUGIN_INPUT_TYPES.includes(id) &&
!EXTERNAL_PLUGIN_INPUT_TYPES.includes(id) &&
!DEBUG_FIELD_GROUP_TYPES.includes(id) &&
!typesInOptionGroup(S, schema, 'v3').includes(id) &&
!typesInOptionGroup(S, schema, '3d').includes(id) &&
!TS_DOC_TYPES.includes(id)
)
}),
...S.documentTypeListItems()
.filter((listItem) => {
const id = listItem.getId()

return (
id &&
!CI_INPUT_TYPES.includes(id) &&
!DEBUG_INPUT_TYPES.includes(id) &&
!STANDARD_INPUT_TYPES.includes(id) &&
!STANDARD_PORTABLE_TEXT_INPUT_TYPES.includes(id) &&
!PLUGIN_INPUT_TYPES.includes(id) &&
!EXTERNAL_PLUGIN_INPUT_TYPES.includes(id) &&
!DEBUG_FIELD_GROUP_TYPES.includes(id) &&
!typesInOptionGroup(S, schema, 'v3').includes(id) &&
!typesInOptionGroup(S, schema, '3d').includes(id) &&
!TS_DOC_TYPES.includes(id)
)
})
.map((listItem) => {
// Create Sheet List menu option
const listItemId = listItem.getId()
if (!listItemId) return listItem

return listItem.child(
S.documentTypeList(listItemId).menuItems([
S.menuItem()
.title('Sheet view')
.group('layout')
.action('setLayout')
.params({layout: 'sheetList'})
.icon(UlistIcon),
...(S.documentTypeList(listItemId).getMenuItems() || []),
]),
)
}),
])
}

Expand Down
8 changes: 8 additions & 0 deletions packages/sanity/src/core/components/previews/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,14 @@ export type PortableTextPreviewLayoutKey = 'block' | 'blockImage' | 'inline'
*/
export type GeneralPreviewLayoutKey = 'compact' | 'default' | 'media' | 'detail'

/**
* General Document list layout key
*
* @hidden
* @beta
*/
export type GeneralDocumentListLayoutKey = GeneralPreviewLayoutKey | 'sheetList'

/**
* Preview layout key. See also {@link GeneralPreviewLayoutKey} and {@link PortableTextPreviewLayoutKey}
*
Expand Down
206 changes: 51 additions & 155 deletions packages/sanity/src/structure/panes/documentList/DocumentListPane.tsx
Original file line number Diff line number Diff line change
@@ -1,40 +1,33 @@
import {SearchIcon, SpinnerIcon} from '@sanity/icons'
import {Box, Card, Code, TextInput} from '@sanity/ui'
import {isEqual} from 'lodash'
import {Box, TextInput} from '@sanity/ui'
import {memo, useCallback, useEffect, useMemo, useRef, useState} from 'react'
import {useObservableCallback} from 'react-rx'
import {debounce, map, type Observable, of, tap, timer} from 'rxjs'
import {
type GeneralPreviewLayoutKey,
SourceProvider,
useI18nText,
useSchema,
useSource,
useTranslation,
useUnique,
} from 'sanity'
import shallowEquals from 'shallow-equals'
import {keyframes, styled} from 'styled-components'

import {Pane} from '../../components/pane'
import {_DEBUG} from '../../constants'
import {structureLocaleNamespace} from '../../i18n'
import {type PaneMenuItem} from '../../types'
import {useStructureToolSetting} from '../../useStructureToolSetting'
import {type BaseStructureToolPaneProps} from '../types'
import {DEFAULT_ORDERING, EMPTY_RECORD} from './constants'
import {EMPTY_RECORD} from './constants'
import {DocumentListPaneContent} from './DocumentListPaneContent'
import {DocumentListPaneHeader} from './DocumentListPaneHeader'
import {applyOrderingFunctions, findStaticTypesInFilter} from './helpers'
import {useShallowUnique} from './PaneContainer'
import {type LoadingVariant, type SortOrder} from './types'
import {useDocumentList} from './useDocumentList'

/**
* @internal
*/
export type DocumentListPaneProps = BaseStructureToolPaneProps<'documentList'>

const EMPTY_ARRAY: never[] = []
export type DocumentListPaneProps = BaseStructureToolPaneProps<'documentList'> & {
sortOrder?: SortOrder
layout?: Exclude<GeneralPreviewLayoutKey, 'sheetList'>
}

const rotate = keyframes`
from {
Expand All @@ -49,71 +42,24 @@ const AnimatedSpinnerIcon = styled(SpinnerIcon)`
animation: ${rotate} 500ms linear infinite;
`

function useShallowUnique<ValueType>(value: ValueType): ValueType {
const valueRef = useRef<ValueType>(value)
if (!shallowEquals(valueRef.current, value)) {
valueRef.current = value
}
return valueRef.current
}

const addSelectedStateToMenuItems = (options: {
menuItems?: PaneMenuItem[]
sortOrderRaw?: SortOrder
layout?: GeneralPreviewLayoutKey
}) => {
const {menuItems, sortOrderRaw, layout} = options

return menuItems?.map((item) => {
if (item.params?.layout) {
return {
...item,
selected: layout === item.params?.layout,
}
}

if (item?.params?.by) {
return {
...item,
selected: isEqual(sortOrderRaw?.by, item?.params?.by || EMPTY_ARRAY),
}
}

return {...item, selected: false}
})
}

/**
* @internal
*/

export const DocumentListPane = memo(function DocumentListPane(props: DocumentListPaneProps) {
const {childItemId, index, isActive, isSelected, pane, paneKey} = props
const {childItemId, isActive, pane, paneKey, sortOrder: sortOrderRaw, layout} = props
const schema = useSchema()
const {name: parentSourceName} = useSource()

const {
defaultLayout = 'default',
displayOptions,
initialValueTemplates = EMPTY_ARRAY,
menuItemGroups,
menuItems,
options,
} = pane
const {apiVersion, defaultOrdering = EMPTY_ARRAY, filter} = options
const {displayOptions, options} = pane
const {apiVersion, filter} = options
const params = useShallowUnique(options.params || EMPTY_RECORD)
const sourceName = pane.source
const typeName = useMemo(() => {
const staticTypes = findStaticTypesInFilter(filter, params)
if (staticTypes?.length === 1) return staticTypes[0]
return null
}, [filter, params])

const showIcons = displayOptions?.showIcons !== false
const [layout, setLayout] = useStructureToolSetting<GeneralPreviewLayoutKey>(
'layout',
typeName ?? pane.id, //pane.id for anything that is not documentTypeList
defaultLayout,
)

const {t} = useTranslation(structureLocaleNamespace)
const {title} = useI18nText(pane)
Expand All @@ -127,17 +73,6 @@ export const DocumentListPane = memo(function DocumentListPane(props: DocumentLi
// We only wan't to show the spinner when the user interacts with the search input.
const showSearchLoadingRef = useRef<boolean>(false)

// Ensure that we use the defaultOrdering value from structure builder if any as the default
const defaultSortOrder = useMemo(() => {
return defaultOrdering?.length > 0 ? {by: defaultOrdering} : DEFAULT_ORDERING
}, [defaultOrdering])

const [sortOrderRaw, setSortOrder] = useStructureToolSetting<SortOrder>(
'sort-order',
typeName ?? pane.id, //pane.id for anything that is not documentTypeList
defaultSortOrder,
)

const sortWithOrderingFn =
typeName && sortOrderRaw
? applyOrderingFunctions(sortOrderRaw, schema.get(typeName) as any)
Expand All @@ -162,16 +97,6 @@ export const DocumentListPane = memo(function DocumentListPane(props: DocumentLi
sortOrder,
})

const menuItemsWithSelectedState = useMemo(
() =>
addSelectedStateToMenuItems({
menuItems,
sortOrderRaw,
layout,
}),
[layout, menuItems, sortOrderRaw],
)

const handleQueryChange = useObservableCallback(
(event$: Observable<React.ChangeEvent<HTMLInputElement>>) => {
return event$.pipe(
Expand Down Expand Up @@ -223,76 +148,47 @@ export const DocumentListPane = memo(function DocumentListPane(props: DocumentLi
return 'initial'
}, [isLoading, items.length])

const searchInput = (
<Box paddingX={3} paddingBottom={3}>
<TextInput
aria-label={t('panes.document-list-pane.search-input.aria-label')}
autoComplete="off"
border={false}
clearButton={Boolean(searchQuery)}
disabled={!isSearchReady}
fontSize={[2, 2, 1]}
icon={loadingVariant === 'spinner' ? AnimatedSpinnerIcon : SearchIcon}
onChange={handleQueryChange}
onClear={handleClearSearch}
onKeyDown={handleSearchKeyDown}
padding={2}
placeholder={t('panes.document-list-pane.search-input.placeholder')}
radius={2}
ref={setSearchInputElement}
spellCheck={false}
value={searchInputValue}
/>
</Box>
)

return (
<SourceProvider name={sourceName || parentSourceName}>
<Pane
currentMaxWidth={350}
data-ui="DocumentListPane"
id={paneKey}
maxWidth={640}
minWidth={320}
selected={isSelected}
>
{_DEBUG && (
<Card padding={4} tone="transparent">
<Code>{pane.source || '(none)'}</Code>
</Card>
)}

<DocumentListPaneHeader
contentAfter={searchInput}
index={index}
initialValueTemplates={initialValueTemplates}
menuItemGroups={menuItemGroups}
menuItems={menuItemsWithSelectedState}
setLayout={setLayout}
setSortOrder={setSortOrder}
title={title}
<>
<Box paddingX={3} paddingBottom={3}>
<TextInput
aria-label={t('panes.document-list-pane.search-input.aria-label')}
autoComplete="off"
border={false}
clearButton={Boolean(searchQuery)}
disabled={!isSearchReady}
fontSize={[2, 2, 1]}
icon={loadingVariant === 'spinner' ? AnimatedSpinnerIcon : SearchIcon}
onChange={handleQueryChange}
onClear={handleClearSearch}
onKeyDown={handleSearchKeyDown}
padding={2}
placeholder={t('panes.document-list-pane.search-input.placeholder')}
radius={2}
ref={setSearchInputElement}
spellCheck={false}
value={searchInputValue}
/>

<DocumentListPaneContent
childItemId={childItemId}
error={error}
filterIsSimpleTypeConstraint={!!typeName}
hasMaxItems={hasMaxItems}
hasSearchQuery={Boolean(searchQuery)}
isActive={isActive}
isLazyLoading={isLazyLoading}
isLoading={isLoading}
items={items}
key={paneKey}
layout={layout}
loadingVariant={loadingVariant}
onListChange={onListChange}
onRetry={onRetry}
paneTitle={title}
searchInputElement={searchInputElement}
showIcons={showIcons}
/>
</Pane>
</SourceProvider>
</Box>
<DocumentListPaneContent
childItemId={childItemId}
error={error}
filterIsSimpleTypeConstraint={!!typeName}
hasMaxItems={hasMaxItems}
hasSearchQuery={Boolean(searchQuery)}
isActive={isActive}
isLazyLoading={isLazyLoading}
isLoading={isLoading}
items={items}
key={paneKey}
layout={layout}
loadingVariant={loadingVariant}
onListChange={onListChange}
onRetry={onRetry}
paneTitle={title}
searchInputElement={searchInputElement}
showIcons={showIcons}
/>
</>
)
})
Original file line number Diff line number Diff line change
Expand Up @@ -277,7 +277,10 @@ export function DocumentListPaneContent(props: DocumentListPaneContentProps) {
])

return (
<PaneContent overflow={layoutCollapsed || loadingVariant === 'initial' ? 'hidden' : 'auto'}>
<PaneContent
data-testid="document-list-pane"
overflow={layoutCollapsed || loadingVariant === 'initial' ? 'hidden' : 'auto'}
>
{mainContent}
</PaneContent>
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import {Text} from '@sanity/ui'

export function DocumentSheetListPane() {
return (
<div data-testid="document-sheet-list-pane">
{/* eslint-disable-next-line i18next/no-literal-string */}
<Text>SheetList view</Text>
</div>
)
}
Loading

0 comments on commit b8f9987

Please sign in to comment.