-
Notifications
You must be signed in to change notification settings - Fork 1
feat: add asset management functions and hooks #656
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
ryanbonial
wants to merge
11
commits into
main
Choose a base branch
from
rb/add-assets
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
448a41b
feat: add asset management functions and hooks
ryanbonial bcff699
chore: fix exports
ryanbonial 2b1a5e7
chore: fix export
ryanbonial 3ac7342
chore: add tests
ryanbonial 105ba04
chore: style asset example
ryanbonial a94f8e7
chore: fix upload example
ryanbonial 48a5540
chore: refactor to use the existing queryStore
ryanbonial 4d635e3
refactor: use @sanity/image-url instead of building a URL function
ryanbonial 4b748c7
Merge branch 'main' into rb/add-assets
ryanbonial 13d827d
chore: allow overriding of asset project and dataset
ryanbonial 5939d25
Merge branch 'main' into rb/add-assets
ryanbonial File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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 hidden or 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,305 @@ | ||
| import { | ||
| type AssetDocumentBase, | ||
| createAssetHandle, | ||
| useAssets, | ||
| useDeleteAsset, | ||
| useLinkMediaLibraryAsset, | ||
| useUploadAsset, | ||
| } from '@sanity/sdk-react' | ||
| import {Button, Card, Flex, Label, Stack, Text} from '@sanity/ui' | ||
| import {type JSX, useCallback, useEffect, useMemo, useRef, useState} from 'react' | ||
|
|
||
| import {DocumentGridLayout} from '../components/DocumentGridLayout/DocumentGridLayout' | ||
| import {PageLayout} from '../components/PageLayout' | ||
|
|
||
| function AssetList({ | ||
| assets, | ||
| onDelete, | ||
| }: { | ||
| assets: AssetDocumentBase[] | ||
| onDelete: (id: string) => void | ||
| }) { | ||
| if (!assets.length) | ||
| return ( | ||
| <Card padding={4} radius={2} tone="inherit"> | ||
| <Stack space={3}> | ||
| <Text weight="semibold">No assets</Text> | ||
| <Text muted size={1}> | ||
| Upload a file to get started. | ||
| </Text> | ||
| </Stack> | ||
| </Card> | ||
| ) | ||
|
|
||
| return ( | ||
| <DocumentGridLayout> | ||
| {assets.map((a) => ( | ||
| <li key={a._id}> | ||
| <Card padding={3} radius={2} tone="inherit" style={{height: '100%'}}> | ||
| <Stack space={3}> | ||
| <Text size={1} muted> | ||
| {a._type} | ||
| </Text> | ||
| {a.url ? ( | ||
| <img | ||
| src={a.url} | ||
| alt={a.originalFilename ?? a._id} | ||
| style={{width: '100%', height: 180, objectFit: 'cover', borderRadius: 4}} | ||
| /> | ||
| ) : ( | ||
| <Card | ||
| padding={3} | ||
| radius={2} | ||
| style={{height: 180, display: 'grid', placeItems: 'center'}} | ||
| > | ||
| <Text size={1} muted> | ||
| {a.originalFilename ?? a._id} | ||
| </Text> | ||
| </Card> | ||
| )} | ||
| <Text size={1} style={{wordBreak: 'break-word'}}> | ||
| {a.originalFilename ?? a._id} | ||
| </Text> | ||
| <Flex gap={2} align="center"> | ||
| <Button | ||
| as="a" | ||
| href={a.url ?? '#'} | ||
| target="_blank" | ||
| rel="noreferrer" | ||
| text="Open" | ||
| mode="bleed" | ||
| /> | ||
| <Flex style={{marginLeft: 'auto'}}> | ||
| <Button tone="critical" text="Delete" onClick={() => onDelete(a._id)} /> | ||
| </Flex> | ||
| </Flex> | ||
| </Stack> | ||
| </Card> | ||
| </li> | ||
| ))} | ||
| </DocumentGridLayout> | ||
| ) | ||
| } | ||
|
|
||
| export function AssetsRoute(): JSX.Element { | ||
| // Query controls | ||
| const [assetType, setAssetType] = useState<'all' | 'image' | 'file'>('all') | ||
| const [order, setOrder] = useState<string>('_createdAt desc') | ||
| const [limit, setLimit] = useState<number>(24) | ||
| // Bump this to force a re-fetch of assets | ||
| const [refresh, setRefresh] = useState(0) | ||
| const [isUploading, setIsUploading] = useState(false) | ||
| const requeryTimeoutsRef = useRef<number[]>([]) | ||
| const options = useMemo( | ||
| () => ({ | ||
| assetType, | ||
| order, | ||
| limit, | ||
| params: {refresh}, // add refresh to params to cache bust | ||
| projectId: 'vo1ysemo', | ||
| dataset: 'production', | ||
| }), | ||
| [assetType, order, limit, refresh], | ||
| ) | ||
|
|
||
| // Hooks | ||
| const assets = useAssets(options) | ||
| const upload = useUploadAsset() | ||
| const remove = useDeleteAsset() | ||
| const linkML = useLinkMediaLibraryAsset() | ||
|
|
||
| // Cleanup any scheduled re-queries on unmount | ||
| useEffect(() => { | ||
| return () => { | ||
| requeryTimeoutsRef.current.forEach((id) => clearTimeout(id)) | ||
| requeryTimeoutsRef.current = [] | ||
| } | ||
| }, []) | ||
|
|
||
| const triggerRequeryBurst = useCallback(() => { | ||
| setRefresh((r) => r + 1) | ||
| const t1 = window.setTimeout(() => setRefresh((r) => r + 1), 600) | ||
| const t2 = window.setTimeout(() => setRefresh((r) => r + 1), 1500) | ||
| requeryTimeoutsRef.current.push(t1, t2) | ||
| }, []) | ||
|
|
||
| // Upload handlers | ||
| const fileInputRef = useRef<HTMLInputElement>(null) | ||
| const onSelectFile = useCallback( | ||
| async (ev: React.ChangeEvent<HTMLInputElement>) => { | ||
| const f = ev.target.files?.[0] | ||
| if (!f) return | ||
| setIsUploading(true) | ||
| const kind = f.type.startsWith('image/') ? 'image' : 'file' | ||
| try { | ||
| if (kind === 'image') { | ||
| await upload('image', f, { | ||
| filename: f.name, | ||
| projectId: options.projectId, | ||
| dataset: options.dataset, | ||
| }) | ||
| } else { | ||
| await upload('file', f, { | ||
| filename: f.name, | ||
| projectId: options.projectId, | ||
| dataset: options.dataset, | ||
| }) | ||
| } | ||
| // trigger re-queries so the new asset appears once indexed | ||
| triggerRequeryBurst() | ||
| } finally { | ||
| if (fileInputRef.current) fileInputRef.current.value = '' | ||
| setIsUploading(false) | ||
| } | ||
| }, | ||
| [upload, triggerRequeryBurst, options.projectId, options.dataset], | ||
| ) | ||
|
|
||
| const onDelete = useCallback( | ||
| async (id: string) => { | ||
| if (!confirm('Delete this asset?')) return | ||
| await remove( | ||
| createAssetHandle({assetId: id, projectId: options.projectId, dataset: options.dataset}), | ||
| ) | ||
| // re-query after deletion | ||
| triggerRequeryBurst() | ||
| }, | ||
| [remove, triggerRequeryBurst, options.projectId, options.dataset], | ||
| ) | ||
|
|
||
| // Media Library link form | ||
| const [mlAssetId, setMlAssetId] = useState('') | ||
| const [mlId, setMlId] = useState('') | ||
| const [mlInstId, setMlInstId] = useState('') | ||
| const onLinkMl = useCallback(async () => { | ||
| if (!mlAssetId || !mlId || !mlInstId) return | ||
| await linkML({assetId: mlAssetId, mediaLibraryId: mlId, assetInstanceId: mlInstId}) | ||
| setMlAssetId('') | ||
| setMlId('') | ||
| setMlInstId('') | ||
| // re-query after linking | ||
| triggerRequeryBurst() | ||
| }, [linkML, mlAssetId, mlId, mlInstId, triggerRequeryBurst]) | ||
|
|
||
| return ( | ||
| <PageLayout title="Assets" subtitle={`${assets.length} assets`}> | ||
| <Stack space={4}> | ||
| <Card padding={4} radius={2} tone="inherit"> | ||
| <div | ||
| className="container-inline" | ||
| style={{ | ||
| display: 'grid', | ||
| gridTemplateColumns: 'repeat(auto-fit, minmax(240px, 1fr))', | ||
| gap: 12, | ||
| }} | ||
| > | ||
| <div> | ||
| <Label size={1} htmlFor="assetType"> | ||
| Type | ||
| </Label> | ||
| <select | ||
| id="assetType" | ||
| value={assetType} | ||
| onChange={(e) => setAssetType(e.target.value as 'all' | 'image' | 'file')} | ||
| style={{ | ||
| width: '100%', | ||
| border: '1px solid #ccc', | ||
| padding: '8px', | ||
| borderRadius: '4px', | ||
| }} | ||
| > | ||
| <option value="all">All</option> | ||
| <option value="image">Images</option> | ||
| <option value="file">Files</option> | ||
| </select> | ||
| </div> | ||
| <div> | ||
| <Label size={1} htmlFor="order"> | ||
| Order | ||
| </Label> | ||
| <input | ||
| id="order" | ||
| value={order} | ||
| onChange={(e) => setOrder(e.target.value)} | ||
| placeholder="_createdAt desc" | ||
| style={{ | ||
| width: '100%', | ||
| border: '1px solid #ccc', | ||
| padding: '8px', | ||
| borderRadius: '4px', | ||
| }} | ||
| /> | ||
| </div> | ||
| <div> | ||
| <Label size={1} htmlFor="limit"> | ||
| Limit | ||
| </Label> | ||
| <input | ||
| id="limit" | ||
| type="number" | ||
| value={limit} | ||
| onChange={(e) => setLimit(parseInt(e.target.value || '0', 10))} | ||
| style={{ | ||
| width: '100%', | ||
| border: '1px solid #ccc', | ||
| padding: '8px', | ||
| borderRadius: '4px', | ||
| }} | ||
| /> | ||
| </div> | ||
| <div> | ||
| <Label size={1} htmlFor="upload"> | ||
| Upload file | ||
| </Label> | ||
| <input | ||
| id="upload" | ||
| ref={fileInputRef} | ||
| onChange={onSelectFile} | ||
| type="file" | ||
| disabled={isUploading} | ||
| /> | ||
| {isUploading && ( | ||
| <Text muted size={1} style={{marginTop: 4, display: 'block'}}> | ||
| Uploading... | ||
| </Text> | ||
| )} | ||
| </div> | ||
| </div> | ||
| </Card> | ||
|
|
||
| <Stack space={3}> | ||
| <Text size={2} weight="semibold"> | ||
| Browse | ||
| </Text> | ||
| <AssetList assets={assets} onDelete={onDelete} /> | ||
| </Stack> | ||
|
|
||
| <Card padding={3} radius={2} tone="inherit"> | ||
| <Stack space={3}> | ||
| <Text size={2} weight="semibold"> | ||
| Link Media Library Asset | ||
| </Text> | ||
| <Flex gap={2} wrap="wrap"> | ||
| <input | ||
| placeholder="assetId" | ||
| value={mlAssetId} | ||
| onChange={(e) => setMlAssetId(e.target.value)} | ||
| /> | ||
| <input | ||
| placeholder="mediaLibraryId" | ||
| value={mlId} | ||
| onChange={(e) => setMlId(e.target.value)} | ||
| /> | ||
| <input | ||
| placeholder="assetInstanceId" | ||
| value={mlInstId} | ||
| onChange={(e) => setMlInstId(e.target.value)} | ||
| /> | ||
| <Button text="Link" onClick={onLinkMl} /> | ||
| </Flex> | ||
| </Stack> | ||
| </Card> | ||
| </Stack> | ||
| </PageLayout> | ||
| ) | ||
| } | ||
This file contains hidden or 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 hidden or 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 |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| /* eslint-disable simple-import-sort/exports */ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why disable here?
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. oops, good catch |
||
| import {type SanityProject as _SanityProject} from '@sanity/client' | ||
|
|
||
| /** | ||
|
|
@@ -52,11 +53,13 @@ export { | |
| export {type AuthConfig, type AuthProvider} from '../config/authConfig' | ||
| export { | ||
| createDatasetHandle, | ||
| createAssetHandle, | ||
| createDocumentHandle, | ||
| createDocumentTypeHandle, | ||
| createProjectHandle, | ||
| } from '../config/handles' | ||
| export { | ||
| type AssetHandle, | ||
| type DatasetHandle, | ||
| type DocumentHandle, | ||
| type DocumentTypeHandle, | ||
|
|
@@ -139,6 +142,22 @@ export { | |
| export {getPerspectiveState} from '../releases/getPerspectiveState' | ||
| export type {ReleaseDocument} from '../releases/releasesStore' | ||
| export {getActiveReleasesState} from '../releases/releasesStore' | ||
| export { | ||
| type AssetDocumentBase, | ||
| type AssetKind, | ||
| type AssetQueryOptions, | ||
| type ImageAssetId, | ||
| type LinkMediaLibraryAssetOptions, | ||
| type UploadAssetOptions, | ||
| getImageUrlBuilder, | ||
| deleteAsset, | ||
| getAssetDownloadUrl, | ||
| getAssetsState, | ||
| isImageAssetId, | ||
| linkMediaLibraryAsset, | ||
| resolveAssets, | ||
| uploadAsset, | ||
| } from '../assets/assets' | ||
| export {createSanityInstance, type SanityInstance} from '../store/createSanityInstance' | ||
| export {type Selector, type StateSource} from '../store/createStateSourceAction' | ||
| export {getUsersKey, parseUsersKey} from '../users/reducers' | ||
|
|
||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
Curious if we still need this after the move to queryStore? Or if this is logic we could already bake in to our assets store? My personal preference is that our usage of hooks in Kitchensink is pretty "vanilla" (mostly for e2e tests to be accurate)