Skip to content

Commit

Permalink
feat: add initial file custom asset source support: conditionally fil…
Browse files Browse the repository at this point in the history
…ter and facet by asset type
  • Loading branch information
robinpyon committed Sep 21, 2021
1 parent 696e8b8 commit 7a2b988
Show file tree
Hide file tree
Showing 13 changed files with 142 additions and 73 deletions.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
"@reduxjs/toolkit": "1.5.0",
"@sanity/color": "2.0.12",
"@sanity/icons": "1.0.2",
"@sanity/types": "^2.17.1",
"@sanity/ui": ">=0.33.11",
"@tanem/react-nprogress": "3.0.52",
"copy-to-clipboard": "^3.3.1",
Expand Down
4 changes: 4 additions & 0 deletions sanity.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,10 @@
"implements": "part:@sanity/form-builder/input/image/asset-source",
"path": "index.js"
},
{
"implements": "part:@sanity/form-builder/input/file/asset-source",
"path": "index.js"
},
{
"implements": "part:@sanity/base/tool",
"path": "index.js"
Expand Down
4 changes: 2 additions & 2 deletions src/app.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import {Box, Portal, PortalProvider, studioTheme, ThemeProvider, ToastProvider} from '@sanity/ui'
import {SanityCustomAssetSourceProps} from '@types'
import {AssetSourceComponentProps} from '@types'
import React, {FC, forwardRef, MouseEvent, Ref} from 'react'
import {ThemeProvider as LegacyThemeProvider} from 'theme-ui'
import Browser from './components/Browser'
Expand All @@ -10,7 +10,7 @@ import useKeyPress from './hooks/useKeyPress'
import GlobalStyle from './styled/GlobalStyles'
import theme from './styled/theme'

type Props = SanityCustomAssetSourceProps
type Props = AssetSourceComponentProps

const AssetBrowser: FC<Props> = forwardRef((props: Props, ref: Ref<HTMLDivElement>) => {
const {onClose, onSelect, tool} = props
Expand Down
10 changes: 5 additions & 5 deletions src/components/Header/index.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import {CloseIcon, Icon, UploadIcon} from '@sanity/icons'
import {Box, Button, Flex, Inline, Text} from '@sanity/ui'
import pluralize from 'pluralize'
import React, {FC} from 'react'

import {useAssetSourceActions} from '../../contexts/AssetSourceDispatchContext'
import {useDropzoneActions} from '../../contexts/DropzoneDispatchContext'
import useTypedSelector from '../../hooks/useTypedSelector'
Expand All @@ -14,12 +14,12 @@ const Header: FC<Props> = (props: Props) => {
const {onClose} = props

const {open} = useDropzoneActions()
const {onSelect} = useAssetSourceActions()

// Redux
const assetTypes = useTypedSelector(state => state.assets.assetTypes)
const selectedDocument = useTypedSelector(state => state.selected.document)

const {onSelect} = useAssetSourceActions()

// Row: Current document / close button
return (
<Box paddingY={2}>
Expand All @@ -28,7 +28,7 @@ const Header: FC<Props> = (props: Props) => {
<Box flex={1} marginX={3}>
<Inline style={{whiteSpace: 'nowrap'}}>
<Text textOverflow="ellipsis" weight="semibold">
<span>{onSelect ? 'Insert Image' : 'Browse Assets'}</span>
<span>{onSelect ? `Insert ${assetTypes.join(' or ')}` : 'Browse Assets'}</span>
</Text>

{selectedDocument && (
Expand All @@ -51,7 +51,7 @@ const Header: FC<Props> = (props: Props) => {
icon={UploadIcon}
mode="bleed"
onClick={open}
text={`Upload ${selectedDocument ? 'images' : 'assets'}`}
text={`Upload ${assetTypes.length === 1 ? pluralize(assetTypes[0]) : 'assets'}`}
tone="primary"
/>

Expand Down
13 changes: 9 additions & 4 deletions src/components/ReduxProvider/index.tsx
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import {Store, configureStore, getDefaultMiddleware, AnyAction} from '@reduxjs/toolkit'
import {AnyAction, configureStore, getDefaultMiddleware, Store} from '@reduxjs/toolkit'
import {AssetSourceComponentProps} from '@types'
import React, {Component, ReactNode} from 'react'
import {Provider} from 'react-redux'
import {createEpicMiddleware} from 'redux-observable'

import {rootEpic, rootReducer} from '../../modules'
import {initialState as assetsInitialState} from '../../modules/assets'
// import {assetsActions} from '../../modules/assets'
// import {searchActions} from '../../modules/search'
// import {uploadsActions} from '../../modules/uploads'
import {RootReducerState} from '../../modules/types'
import {SanityCustomAssetSourceProps} from '../../types'
import getDocumentAssetIds from '../../utils/getDocumentAssetIds'

type Props = SanityCustomAssetSourceProps
type Props = AssetSourceComponentProps

class ReduxProvider extends Component<Props> {
store: Store

Expand Down Expand Up @@ -41,6 +42,10 @@ class ReduxProvider extends Component<Props> {
],
devTools: true,
preloadedState: {
assets: {
...assetsInitialState,
assetTypes: props?.assetType ? [props.assetType] : ['file', 'image']
},
selected: {
assets: props.selectedAssets,
document: props.document,
Expand Down
50 changes: 32 additions & 18 deletions src/components/SearchFacetsControl/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,38 +3,52 @@ import {Button, Flex, Menu, MenuButton, MenuDivider, MenuGroup, MenuItem} from '
import {SearchFacetDivider, SearchFacetGroup, SearchFacetInputProps} from '@types'
import React, {FC} from 'react'
import {useDispatch} from 'react-redux'

import {FACETS} from '../../constants'
import useTypedSelector from '../../hooks/useTypedSelector'
import {searchActions} from '../../modules/search'

const SearchFacetsControl: FC = () => {
// Redux
const dispatch = useDispatch()
const assetTypes = useTypedSelector(state => state.assets.assetTypes)
const searchFacets = useTypedSelector(state => state.search.facets)
const selectedDocument = useTypedSelector(state => state.selected.document)

const isTool = !selectedDocument

// Manually filter facets based on current context, whether it's invoked as a tool, or via selection through
// via custom asset source.
const filteredFacets = FACETS.filter(facet => {
if (facet.type === 'group' || facet.type === 'divider') {
return true
}
const filteredFacets = FACETS
// Filter facets based on current context, whether it's invoked as a tool, or via selection through via custom asset source.
.filter(facet => {
if (facet.type === 'group' || facet.type === 'divider') {
return true
}

if (isTool) {
return !facet?.selectOnly
} else {
// TODO: in future, determine whether we're inserting into a file or image field.
// For now, it's only possible to insert into image fields.
return facet.assetTypes.includes('image')
}
})
if (isTool) {
return !facet?.selectOnly
} else {
const matchingAssetTypes = facet.assetTypes.filter(assetType =>
assetTypes.includes(assetType)
)
return matchingAssetTypes.length > 0
}
})
// Remove adjacent and leading / trailing dividers
.filter((facet, index, facets) => {
const previousFacet = facets[index - 1]
// Ignore leading + trailing dividers
if ((facet.type === 'divider' && index === 0) || index === facets.length - 1) {
return false
}
// Ignore adjacent dividers
if (facet.type === 'divider' && previousFacet?.type === 'divider') {
return false
}
return true
})

// Determine if there are any remaining facets
// Determine if there are any remaining un-selected facets
// (This operates under the assumption that only one of each facet can be active at any given time)
const remainingSearchFacets =
const hasRemainingSearchFacets =
filteredFacets.filter(facet => facet).length - searchFacets.length > 0

const renderMenuFacets = (
Expand Down Expand Up @@ -77,7 +91,7 @@ const SearchFacetsControl: FC = () => {
<MenuButton
button={
<Button
disabled={!remainingSearchFacets}
disabled={!hasRemainingSearchFacets}
fontSize={1}
icon={AddCircleIcon}
mode="bleed"
Expand Down
21 changes: 16 additions & 5 deletions src/components/UploadDropzone/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import React, {FC, ReactNode} from 'react'
import {DropEvent, useDropzone} from 'react-dropzone'
import {useDispatch} from 'react-redux'
import styled from 'styled-components'

import {useAssetSourceActions} from '../../contexts/AssetSourceDispatchContext'
import {DropzoneDispatchProvider} from '../../contexts/DropzoneDispatchContext'
import useTypedSelector from '../../hooks/useTypedSelector'
import {notificationsActions} from '../../modules/notifications'
import {uploadsActions} from '../../modules/uploads'

Expand Down Expand Up @@ -63,14 +63,24 @@ async function filterFiles(fileList: FileList) {
const UploadDropzone: FC<Props> = (props: Props) => {
const {children} = props

const {onSelect} = useAssetSourceActions()

// Redux
const dispatch = useDispatch()
const assetTypes = useTypedSelector(state => state.assets.assetTypes)

const {onSelect} = useAssetSourceActions()
const isImageAssetType = assetTypes.length === 1 && assetTypes[0] === 'image'

// Callbacks
const handleDrop = async (acceptedFiles: File[]) => {
acceptedFiles.forEach(file => dispatch(uploadsActions.uploadRequest({file})))
acceptedFiles.forEach(file =>
dispatch(
uploadsActions.uploadRequest({
file,
forceAsAssetType: assetTypes.length === 1 ? assetTypes[0] : undefined
})
)
)
}

// Use custom file selector to obtain files on file drop + change events (excluding folders and packages)
Expand Down Expand Up @@ -106,11 +116,12 @@ const UploadDropzone: FC<Props> = (props: Props) => {
return files
}

// Limit file picking to only images if we're specifically within an image selection context (e.g. picking from image fields)
const {getRootProps, getInputProps, isDragActive, open} = useDropzone({
accept: onSelect ? 'image/*' : '',
accept: isImageAssetType ? 'image/*' : '',
getFilesFromEvent: handleFileGetter,
noClick: true,
// Disable drag and drop functionality when in a selecting context
// HACK: Disable drag and drop functionality when in a selecting context
// (This is currently due to Sanity's native image input taking precedence with drag and drop)
noDrag: !!onSelect,
onDrop: handleDrop
Expand Down
6 changes: 3 additions & 3 deletions src/contexts/AssetSourceDispatchContext.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import {SelectedAsset} from '@types'
import React, {ReactNode, createContext, useContext} from 'react'
import type {AssetFromSource} from '@sanity/types'

type ContextProps = {
onSelect?: (assets: SelectedAsset[]) => void
onSelect?: (assets: AssetFromSource[]) => void
}

type Props = {
children: ReactNode
onSelect?: (assets: SelectedAsset[]) => void
onSelect?: (assets: AssetFromSource[]) => void
}

const AssetSourceDispatchContext = createContext<ContextProps | undefined>(undefined)
Expand Down
17 changes: 14 additions & 3 deletions src/modules/assets/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,15 @@
import {AnyAction, PayloadAction, createSelector, createSlice} from '@reduxjs/toolkit'
import type {ClientError, Patch, Transaction} from '@sanity/client'
import {Asset, AssetItem, BrowserView, HttpError, Order, OrderDirection, Tag} from '@types'
import {
Asset,
AssetItem,
AssetType,
BrowserView,
HttpError,
Order,
OrderDirection,
Tag
} from '@types'
import groq from 'groq'
import {nanoid} from 'nanoid'
import {Epic, ofType} from 'redux-observable'
Expand Down Expand Up @@ -34,6 +43,7 @@ type ItemError = {

export type AssetsReducerState = {
allIds: string[]
assetTypes: AssetType[]
byIds: Record<string, AssetItem>
fetchCount: number
fetching: boolean
Expand Down Expand Up @@ -63,8 +73,9 @@ const defaultOrder = ORDER_OPTIONS[0] as {
* of `fetchCount` and reinstate `totalCount` across the board.
*/

const initialState = {
export const initialState = {
allIds: [],
assetTypes: [],
byIds: {},
fetchCount: -1,
fetching: false,
Expand Down Expand Up @@ -449,7 +460,7 @@ export const assetsFetchPageIndexEpic: MyEpic = (action$, state$) =>
const documentAssetIds = state?.selected?.documentAssetIds

const filter = constructFilter({
hasDocument: !!state.selected.document,
assetTypes: state.assets.assetTypes,
searchFacets: state.search.facets,
searchQuery: state.search.query
})
Expand Down
15 changes: 9 additions & 6 deletions src/modules/uploads/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,10 @@ const uploadsSlice = createSlice({
}
delete state.byIds[hash]
},
uploadRequest(_state, _action: PayloadAction<{file: File}>) {
uploadRequest(
_state,
_action: PayloadAction<{file: File; forceAsAssetType?: 'file' | 'image'}>
) {
//
},
uploadProgress(
Expand Down Expand Up @@ -189,25 +192,25 @@ export const uploadsAssetUploadEpic: MyEpic = action$ =>
action$.pipe(
filter(uploadsActions.uploadRequest.match),
mergeMap(action => {
const {file} = action.payload
const {file, forceAsAssetType} = action.payload

return of(action).pipe(
// Generate SHA1 hash from local file
mergeMap(() => hashFile$(file)),
// Ignore if we're unable to generate a hash
// TODO: we may want to throw an error in `hashFile$` and handle this
filter(hash => !!hash),
// Disaptch start action and begin upload process
// Dispatch start action and begin upload process
mergeMap(hash => {
const assetType = forceAsAssetType || (file.type.indexOf('image') >= 0 ? 'image' : 'file')
const uploadItem = {
_type: 'upload',
assetType: file.type.indexOf('image') >= 0 ? 'image' : 'file',
assetType,
hash,
name: file.name,
size: file.size,
status: 'queued'
} as UploadItem

return of(uploadsActions.uploadStart({file, uploadItem}))
})
)
Expand Down Expand Up @@ -236,7 +239,7 @@ export const uploadsCheckRequestEpic: MyEpic = (action$, state$) =>
const documentIds = assets.map(asset => asset._id)

const filter = constructFilter({
hasDocument: !!state.selected.document,
assetTypes: state.assets.assetTypes,
searchFacets: state.search.facets,
searchQuery: state.search.query
})
Expand Down
Loading

0 comments on commit 7a2b988

Please sign in to comment.