import { acceptedFileTypes } from './shared/acceptedFileTypes'; import { Asset } from './types/Asset'; import { AssetModal } from './components/AssetModal'; import { customFields } from './config'; import { DeleteModal } from './components/DeleteModal'; import { MediaLibrary } from './components/MediaLibrary'; import { Sidebar } from './components/Sidebar'; import { SortOption } from './types/SortOption'; import { UploadDropArea } from './components/UploadDropArea'; import sanityClient from 'part:@sanity/base/client'; import React, { useEffect, useState } from 'react'; import styled from 'styled-components'; import { Card, useToast } from '@sanity/ui'; import { ToolType } from './types/ToolType'; const client = sanityClient.withConfig({ apiVersion: '2021-06-19' }); type Props = { onClose?: () => void; onSelect?: (assets: Array) => void; selectedAssets?: Asset[]; tool?: string; mode?: ToolType; }; const StyledContainer = styled.div` background-color: var(--white); color: var(--body-text); height: 100%; left: 0; position: absolute; top: 0; width: 100%; & *, & *:before, & *:after { box-sizing: border-box; } `; const StyledSidebarGridContainer = styled.div` display: flex; height: 100%; `; export const App = ({ onClose, onSelect, selectedAssets, tool, mode }: Props) => { const [activeExtensions, setActiveExtensions] = useState>([]); const [activeTags, setActiveTags] = useState>([]); const [assets, setAssets] = useState>([]); const [assetsToDelete, setAssetsToDelete] = useState | null>(null); const [assetToEdit, setAssetToEdit] = useState(null); const [filteredAssets, setFilteredAssets] = useState>(assets); const [isDraggingMediaItem, setIsDraggingMediaItem] = useState(false); const [loading, setLoading] = useState(true); const [localSelectedAssets, setLocalSelectedAssets] = useState>([]); const [searchQuery, setSearchQuery] = useState(''); const [sort, setSort] = useState('date'); const toast = useToast(); const types = tool ? '"sanity.imageAsset", "sanity.fileAsset"' : '"sanity.imageAsset"'; const includedFields = [ '_createdAt', '_id', '_type', 'alt', 'extension', 'metadata', 'originalFilename', 'title', 'size', 'tags', 'url', '"usedBy": *[references(^._id)] { _id, _type }', ...customFields.map(({ name }: { name: string }) => name), ]; const query = `*[_type in [${types}]] { ${includedFields.join(',')} }`; useEffect(() => { let newFilteredAssets = [...assets]; if (searchQuery && searchQuery !== '') { console.log(newFilteredAssets); newFilteredAssets = newFilteredAssets.filter( ({ _id = '', alt = '', originalFilename = '', title = '', tags = [], attribution = '', description = '' }) => [originalFilename, title, alt, tags?.join(''), _id, attribution, description].some( (value) => value?.replace(/\s/g, '').toUpperCase().indexOf(searchQuery.replace(/\s/g, '').toUpperCase()) > -1 ) ); } if (activeExtensions.length > 0) { newFilteredAssets = newFilteredAssets.filter(({ extension }) => activeExtensions.indexOf(extension) > -1); } if (activeTags.length > 0) { newFilteredAssets = newFilteredAssets.filter( ({ tags, usedBy }) => tags?.some((tag) => activeTags.indexOf(tag) > -1) || (activeTags.indexOf('used') > -1 && usedBy.length) || (activeTags.indexOf('unused') > -1 && !usedBy.length) ); } if (sort === 'date') { newFilteredAssets.sort((a, b) => (a._createdAt > b._createdAt ? -1 : 1)); } if (sort === 'az') { newFilteredAssets.sort((a, b) => (a.title || a.originalFilename).localeCompare(b.title || b.originalFilename)); } if (sort === 'za') { newFilteredAssets.sort((a, b) => (b.title || b.originalFilename).localeCompare(a.title || a.originalFilename)); } if (sort === 'mostUsed') { newFilteredAssets.sort((a, b) => (a.usedBy.length > b.usedBy.length ? -1 : 1)); } setFilteredAssets(newFilteredAssets); // Update selected assets so it does not include assets that are no longer visible const newAssetIds = newFilteredAssets.map(({ _id }) => _id); const newAssetsToSelect = localSelectedAssets.filter(({ _id }) => newAssetIds.indexOf(_id) > -1); setLocalSelectedAssets(newAssetsToSelect); }, [assets, activeExtensions, activeTags, sort, searchQuery]); useEffect(() => { if (assets.length === 0 || localSelectedAssets.length > 0 || selectedAssets?.length === 0) { return; } const selectedAssetsIds = (selectedAssets || []).map(({ _id }) => _id); const assetsToSelect = [...assets].filter(({ _id }) => selectedAssetsIds.indexOf(_id) > -1); setLocalSelectedAssets(assetsToSelect); }, [assets, selectedAssets]); useEffect(subscribeToAssetChanges, []); useEffect(() => { fetchAssets(); }, []); async function fetchAssets() { try { setLoading(true); const newAssets: Array = await client.fetch(query, {}); setAssets(newAssets); } catch (e) { handleError(e); } finally { setLoading(false); } } function subscribeToAssetChanges() { const subscription = client .listen(query) .subscribe( ({ documentId, result, transition, }: { documentId: string; result: Asset; transition: 'disappear' | 'update' | 'appear'; }) => { if (transition === 'disappear') { return setAssets((assets) => [...assets].filter(({ _id }) => _id !== documentId)); } if (transition === 'update') { return setAssets((assets) => [...assets].map((asset) => (asset._id === documentId ? result : asset))); } if (transition === 'appear') { return setAssets((assets) => [...assets, result]); } } ); return () => subscription.unsubscribe(); } async function onUpload(files: FileList) { try { setLoading(true); const filesWithAllowedFileType: Array = Array.from(files).filter(({ type }) => acceptedFileTypes.some((fileType) => new RegExp(fileType, 'gi').test(type)) ); const notAllowedFiles = Array.from(files).filter((file) => filesWithAllowedFileType.indexOf(file) === -1); notAllowedFiles.forEach(({ name }) => { handleError(`File '${name}' will not be uploaded because its filetype is not supported.`); }); await Promise.all( filesWithAllowedFileType.map((file) => client.assets.upload(file.type.indexOf('image') > -1 ? 'image' : 'file', file) ) ); } catch (e: any) { handleError(e); } finally { setLoading(false); } } function handleSelect(selectedAssets: Array) { if (onSelect && selectedAssets.length > 0) { const assetsWithDocumentIds = selectedAssets.map(({ _id }) => ({ kind: 'assetDocumentId', value: _id })); onSelect(assetsWithDocumentIds); } } function handleError(error: any) { console.error(error); toast.push({ status: 'error', title: error.toString(), }); } const onExtensionClick = (value: string) => onFilterClick(value, activeExtensions, setActiveExtensions); const onTagClick = (value: string) => onFilterClick(value, activeTags, setActiveTags); function onClearFilters() { setActiveExtensions([]); setActiveTags([]); } async function onTagDrop(tag: string) { try { if (loading || localSelectedAssets.length === 0) { return; } setLoading(true); const idsAndTags = localSelectedAssets.map(({ _id, tags }) => ({ _id, tags })); const idsWithNewTags = idsAndTags.map(({ _id, tags }) => ({ _id, tags: tags?.includes(tag) ? tags : [...(tags || []), tag], })); await Promise.all(idsWithNewTags.map(({ _id, tags }) => client.patch(_id).set({ tags }).commit())); } catch (e) { handleError(e); } finally { setLoading(false); } } const extensions: Array<{ isActive: boolean; value: string }> = getUniqueFiltersWithActive( assets, activeExtensions, (acc, { extension }) => [...acc, extension] ); const tags: Array<{ isActive: boolean; value: string }> = [ { isActive: activeTags.indexOf('used') > -1, value: 'used' }, { isActive: activeTags.indexOf('unused') > -1, value: 'unused' }, ...getUniqueFiltersWithActive(assets, activeTags, (acc, { tags }) => (tags ? [...acc, ...tags] : acc)), ]; return ( {mode === 'tool' && ( )} {mode === 'tool' && ( <> {assetToEdit && ( setAssetToEdit(null)} onSaveComplete={() => { setAssetToEdit(null); }} setLoading={setLoading} /> )} {assetsToDelete && ( setAssetsToDelete(null)} onDeleteComplete={() => { setAssetsToDelete(null); }} setLoading={setLoading} /> )} )} ); }; function getUniqueFiltersWithActive( array: Array, activeArray: Array, reduceFn: (acc: Array, cur: any) => Array ): Array<{ isActive: boolean; value: any }> { return Array.from(new Set(array.reduce(reduceFn, []))) .filter(Boolean) .sort() .map((value) => ({ isActive: activeArray.indexOf(value) > -1, value })); } function onFilterClick(value: string, stateValue: Array, setStateValue: (value: Array) => void) { const index = stateValue.indexOf(value); if (index === -1) { return setStateValue([...stateValue, value]); } const newValues = [...stateValue]; newValues.splice(index, 1); setStateValue(newValues); }