Skip to content

Commit

Permalink
fix(tags): render all tags in a virtualized container
Browse files Browse the repository at this point in the history
  • Loading branch information
robinpyon committed Sep 21, 2021
1 parent e71ae9f commit abdc7e3
Show file tree
Hide file tree
Showing 12 changed files with 222 additions and 212 deletions.
9 changes: 4 additions & 5 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import {Box, Portal, ThemeProvider, ToastProvider, studioTheme, PortalProvider} from '@sanity/ui'
import {Box, Portal, PortalProvider, studioTheme, ThemeProvider, ToastProvider} from '@sanity/ui'
import {SanityCustomAssetSourceProps} from '@types'
import React, {FC, MouseEvent, forwardRef, Ref} from 'react'
import React, {FC, forwardRef, MouseEvent, Ref} from 'react'
import {ThemeProvider as LegacyThemeProvider} from 'theme-ui'

import {Z_INDEX_APP, Z_INDEX_TOAST_PROVIDER} from './constants'
import {AssetBrowserDispatchProvider} from './contexts/AssetSourceDispatchContext'
import Browser from './components/Browser'
import ReduxProvider from './components/ReduxProvider'
import {Z_INDEX_APP, Z_INDEX_TOAST_PROVIDER} from './constants'
import {AssetBrowserDispatchProvider} from './contexts/AssetSourceDispatchContext'
import useKeyPress from './hooks/useKeyPress'
import GlobalStyle from './styled/GlobalStyles'
import theme from './styled/theme'
Expand Down
9 changes: 4 additions & 5 deletions src/components/Cards/index.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,12 @@
import {CardAssetData, CardUploadData} from '@types'
import React, {CSSProperties, ReactNode, Ref, forwardRef, memo} from 'react'
import React, {CSSProperties, forwardRef, memo, ReactNode, Ref} from 'react'
import {
GridOnItemsRenderedProps,
GridChildComponentProps,
areEqual,
FixedSizeGrid,
areEqual
GridChildComponentProps,
GridOnItemsRenderedProps
} from 'react-window'
import {Box} from 'theme-ui'

import useTypedSelector from '../../hooks/useTypedSelector'
import CardAsset from '../CardAsset'
import CardUpload from '../CardUpload'
Expand Down
8 changes: 5 additions & 3 deletions src/components/DialogTags/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import {useDispatch} from 'react-redux'

import {Z_INDEX_DIALOG} from '../../constants'
import {dialogActions} from '../../modules/dialog'
import Tags from '../Tags'
import TagView from '../TagView'

type Props = {
children: ReactNode
Expand All @@ -31,10 +31,12 @@ const DialogTags: FC<Props> = (props: Props) => {
<Dialog header="All Tags" id={id} onClose={handleClose} width={1} zOffset={Z_INDEX_DIALOG}>
<Box
style={{
borderTop: `1px solid ${hues.gray?.[900].hex}`
borderTop: `1px solid ${hues.gray?.[900].hex}`,
height: '100%',
minHeight: '320px'
}}
>
<Tags />
<TagView />
</Box>

{children}
Expand Down
3 changes: 1 addition & 2 deletions src/components/Items/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,8 @@ import {Box, Text} from '@sanity/ui'
import React, {FC, Ref, useEffect} from 'react'
import {useDispatch} from 'react-redux'
import AutoSizer from 'react-virtualized-auto-sizer'
import {ListOnItemsRenderedProps, GridOnItemsRenderedProps} from 'react-window'
import {GridOnItemsRenderedProps, ListOnItemsRenderedProps} from 'react-window'
import InfiniteLoader from 'react-window-infinite-loader'

import {PANEL_HEIGHT} from '../../constants'
import useBreakpointIndex from '../../hooks/useBreakpointIndex'
import useTypedSelector from '../../hooks/useTypedSelector'
Expand Down
3 changes: 1 addition & 2 deletions src/components/Table/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,6 @@ const Table = forwardRef((props: Props, ref: Ref<any>) => {
const selectedAssets = useTypedSelector(state => state.selected.assets)

const selectedIds = (selectedAssets && selectedAssets.map(asset => asset._id)) || []
const totalCount = items.length

const itemKey = (index: number) => {
const item = items[index]
Expand All @@ -85,7 +84,7 @@ const Table = forwardRef((props: Props, ref: Ref<any>) => {
items,
selectedIds
}}
itemCount={totalCount}
itemCount={items.length}
itemKey={itemKey}
itemSize={100} // px
onItemsRendered={onItemsRendered}
Expand Down
41 changes: 0 additions & 41 deletions src/components/TagPanel/index.tsx

This file was deleted.

48 changes: 48 additions & 0 deletions src/components/TagView/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {black, hues} from '@sanity/color'
import {Box, Flex, Text} from '@sanity/ui'
import React, {FC} from 'react'
import useTypedSelector from '../../hooks/useTypedSelector'
import {selectAssetsPickedLength} from '../../modules/assets'
import {selectTags} from '../../modules/tags'
import TagViewHeader from '../TagViewHeader'
import Tags from '../Tags'

const TagView: FC = () => {
const numPickedAssets = useTypedSelector(selectAssetsPickedLength)
const tags = useTypedSelector(selectTags)
const fetching = useTypedSelector(state => state.tags.fetching)
const fetchCount = useTypedSelector(state => state.tags.fetchCount)
const fetchComplete = fetchCount !== -1
const hasTags = !fetching && tags?.length > 0
const hasPicked = !!(numPickedAssets > 0)

return (
<Flex
direction="column"
flex={1}
style={{
// background: hues.gray[950].hex,
background: black.hex,
height: '100%'
}}
>
<TagViewHeader
allowCreate //
light={hasPicked}
title={hasPicked ? 'Tags (in selection)' : 'Tags'}
/>

{fetchComplete && !hasTags && (
<Box padding={3}>
<Text size={1} style={{color: hues.gray[700].hex}}>
<em>No tags</em>
</Text>
</Box>
)}

{hasTags && <Tags />}
</Flex>
)
}

export default TagView
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import {TagActions, TagItem} from '@types'
import React, {FC} from 'react'
import {useDispatch} from 'react-redux'
import {PANEL_HEIGHT} from '../../constants'

import useTypedSelector from '../../hooks/useTypedSelector'
import {dialogActions} from '../../modules/dialog'
import Tag from '../Tag'
Expand All @@ -18,7 +17,7 @@ type Props = {
title: string
}

const PanelHeader: FC<Props> = (props: Props) => {
const TagViewHeader: FC<Props> = (props: Props) => {
const {actions, allowCreate, light, tags, title} = props

// Redux
Expand All @@ -40,10 +39,7 @@ const PanelHeader: FC<Props> = (props: Props) => {
style={{
background: light ? hues.gray?.[900].hex : black.hex,
borderBottom: `1px solid ${hues.gray?.[900].hex}`,
height: `${PANEL_HEIGHT}px`,
position: 'sticky',
top: 0,
zIndex: 1 // force stacking context
height: `${PANEL_HEIGHT}px`
}}
>
<Inline space={2}>
Expand Down Expand Up @@ -84,4 +80,4 @@ const PanelHeader: FC<Props> = (props: Props) => {
)
}

export default PanelHeader
export default TagViewHeader
172 changes: 154 additions & 18 deletions src/components/Tags/index.tsx
Original file line number Diff line number Diff line change
@@ -1,26 +1,162 @@
import {black} from '@sanity/color'
import {Box} from '@sanity/ui'
import React, {FC} from 'react'

import {Box, Flex, Label} from '@sanity/ui'
import React, {FC, memo} from 'react'
import AutoSizer from 'react-virtualized-auto-sizer'
import {areEqual, FixedSizeList, ListChildComponentProps} from 'react-window'
import useTypedSelector from '../../hooks/useTypedSelector'
import {selectAssetsPickedLength} from '../../modules/assets'
import TagsAll from '../TagsAll'
import TagsPicked from '../TagsPicked'
import {selectAssetsPicked} from '../../modules/assets'
import {selectTags} from '../../modules/tags'
import {TagActions, TagItem} from '../../types'
import Tag from '../Tag'

const VirtualRow = memo((props: ListChildComponentProps) => {
const {data, index, style} = props
const {items} = data
const item = items[index]

// Render label
if (typeof item === 'string') {
return (
<Flex align="center" justify="space-between" key={item} paddingY={3} style={style}>
<Label size={0}>{item}</Label>
</Flex>
)
}

// Render tag
return (
<Box key={item.tag?._id} style={style}>
<Tag actions={item.actions} tag={item} />
</Box>
)
}, areEqual)

const Tags: FC = () => {
const numPickedAssets = useTypedSelector(selectAssetsPickedLength)
const assetsPicked = useTypedSelector(selectAssetsPicked)
const tags = useTypedSelector(selectTags)

// TODO: refactor, there's most certainly a more performant way to do this

// Filter out all tag IDS used (across all) and dedupe
const pickedTagIds = assetsPicked?.reduce((acc: string[], val) => {
const assetTagIds = val?.asset?.opt?.media?.tags?.map(tag => tag._ref) || []
acc = acc.concat(assetTagIds)
return acc
}, [])
const pickedTagIdsUnique = [...new Set(pickedTagIds)]

// Segment tags into two buckets:
// 1. those which exist in all picked assets ('applied to all')
// 2. those which exist in some picked assets ('applied to some')
const tagIdsSegmented = pickedTagIdsUnique.reduce(
(acc: {appliedToAll: string[]; appliedToSome: string[]}, tagId) => {
const tagIsInEveryAsset = assetsPicked.every(assetItem => {
const tagIndex =
assetItem.asset.opt?.media?.tags?.findIndex(tag => tag._ref === tagId) ?? -1
if (tagIndex >= 0) {
return true
}
})

if (tagIsInEveryAsset) {
acc.appliedToAll.push(tagId)
} else {
acc.appliedToSome.push(tagId)
}

return acc
},
{
appliedToAll: [],
appliedToSome: []
}
)

const tagsAppliedToAll = tags
.filter(tag => tagIdsSegmented.appliedToAll.includes(tag.tag._id))
.map(tagItem => ({
...tagItem,
actions: ['delete', 'edit', 'removeAll', 'search'] as TagActions[]
}))
const tagsAppliedToSome = tags
.filter(tag => tagIdsSegmented.appliedToSome.includes(tag.tag._id))
.map(tagItem => ({
...tagItem,
actions: ['applyAll', 'delete', 'edit', 'removeAll', 'search'] as TagActions[]
}))
const tagsUnused = tags
.filter(tag => !pickedTagIdsUnique.includes(tag.tag._id))
.map(tagItem => ({
...tagItem,
actions: ['applyAll', 'delete', 'edit', 'search'] as TagActions[]
}))

let items: (
| string
| (TagItem & {
actions: TagActions[]
})
)[] = []
if (assetsPicked.length === 0) {
items = tags.map(tagItem => ({
...tagItem,
actions: ['delete', 'edit', 'search'] as TagActions[]
}))
} else {
if (tagsAppliedToAll?.length > 0) {
items = [
...items, //
assetsPicked.length === 1 ? 'Used' : 'Used by all',
...tagsAppliedToAll
]
}
if (tagsAppliedToSome?.length > 0) {
items = [
...items, //
'Used by some',
...tagsAppliedToSome
]
}
if (tagsUnused?.length > 0) {
items = [
...items, //
'Unused',
...tagsUnused
]
}
}

const itemKey = (index: number) => {
const item = items[index]
if (typeof item === 'string') {
return item
}
return item?.tag._id
}

return (
<Box
flex={1}
style={{
// background: hues.gray[950].hex,
background: black.hex,
height: '100%'
}}
>
{numPickedAssets > 0 ? <TagsPicked /> : <TagsAll />}
{/* <TagsPicked /> */}
<Box paddingX={3} style={{flex: 1}}>
<AutoSizer>
{({height, width}) => {
return (
<FixedSizeList
className="media__custom-scrollbar"
height={height}
itemData={{items}}
itemCount={items.length}
itemKey={itemKey}
itemSize={33} // px
style={{
position: 'relative',
overflowX: 'hidden',
overflowY: 'scroll'
}}
width={width}
>
{VirtualRow}
</FixedSizeList>
)
}}
</AutoSizer>
</Box>
)
}
Expand Down
Loading

0 comments on commit abdc7e3

Please sign in to comment.