import { ChevronDownIcon, PublishIcon, ResetIcon, TrashIcon, UnpublishIcon, } from '@sanity/icons'; import { Button, Dialog, Menu, MenuButton, MenuDivider, MenuItem, useToast, } from '@sanity/ui'; import { nanoid } from 'nanoid'; import { useMemo, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; import { Preview } from 'sanity'; import styled from 'styled-components'; import { useBulkActionsTableContext } from '../context'; import { handleBulkOperationError } from '../utils/errorHandling'; const Content = styled.div` padding: 0 1rem; font-size: 0.85rem; `; const Footer = styled.div` display: flex; align-items: center; gap: 0.5rem; justify-content: flex-end; padding: 0.75rem; `; interface Props { onDelete: () => void; } const ErroredDocuments = ({ e, schemaType }: { e: any; schemaType: any }) => { const idsWithErrors: string[] = 'details' in e ? e.details.items.map((item: any) => item.error.id) : []; if (!idsWithErrors.length) { return null; } const plural = idsWithErrors.length !== 1; return (

Please unselect {plural ? 'these' : 'this'} document{plural ? 's' : ''}{' '} and try again:

{idsWithErrors.map((id) => ( ))}

); }; const removeDraftPrefix = (s: string) => s.startsWith('drafts.') ? s.substring('drafts.'.length) : s; function BulkActionsMenu({ onDelete }: Props) { const { options: { client }, selectedIds, setSelectedIds, schemaType, } = useBulkActionsTableContext(); const buttonId = useMemo(nanoid, []); const toast = useToast(); const dialogId = useMemo(nanoid, []); const [dialogMode, setDialogMode] = useState< 'discard_changes' | 'unpublish' | 'publish' | 'delete' | null >(null); const [loading, setLoading] = useState(false); const handleDiscardChanges = async () => { setLoading(true); try { const ids = await client.fetch('*[_id in $ids]._id', { ids: Array.from(selectedIds) .map((id) => [id, `drafts.${id}`]) .flat(), }); const idSet = ids.reduce((set, id) => { set.add(id); return set; }, new Set()); const draftIdsThatAlsoHavePublishedIds = ids.filter( (id) => id.startsWith('drafts.') && idSet.has(id.substring('drafts.'.length)), ); const t = client.transaction(); for (const id of draftIdsThatAlsoHavePublishedIds) { t.delete(id); } await t.commit(); toast.push({ title: 'Changes Discarded', description: `${selectedIds.size} documents reverted`, status: 'success', closable: true, duration: 10 * 1000, }); setSelectedIds(new Set()); setDialogMode(null); } catch (e) { handleBulkOperationError(e, 'discard changes', Array.from(selectedIds)); toast.push({ title: 'Error Bulk Discarding Changes', description: ( <>

The bulk discard changes failed.

), status: 'error', closable: true, duration: 30 * 1000, }); } finally { setLoading(false); } }; const handleUnpublish = async () => { setLoading(true); try { const publishedDocuments = await client.fetch('*[_id in $ids]', { ids: Array.from(selectedIds), }); const t = client.transaction(); for (const publishedDocument of publishedDocuments) { t.createIfNotExists({ ...publishedDocument, _id: `drafts.${publishedDocument._id}`, _updatedAt: new Date().toISOString(), }); t.delete(publishedDocument._id); } await t.commit(); toast.push({ title: 'Unpublish Successful', description: `${selectedIds.size} documents unpublished`, status: 'success', closable: true, duration: 10 * 1000, }); setSelectedIds(new Set()); } catch (e) { handleBulkOperationError(e, 'unpublish', Array.from(selectedIds)); toast.push({ title: 'Error Bulk Unpublishing', description: ( <>

The bulk unpublished failed. This usually occurs because there are other documents referencing the documents you’re trying to unpublish.

), status: 'error', closable: true, duration: 30 * 1000, }); } finally { setDialogMode(null); setLoading(false); } }; const handlePublish = async () => { setLoading(true); try { const draftDocuments = await client.fetch('*[_id in $ids]', { ids: Array.from(selectedIds).map((id) => `drafts.${id}`), }); const t = client.transaction(); for (const draftDocument of draftDocuments) { t.createOrReplace({ ...draftDocument, _id: removeDraftPrefix(draftDocument._id), _updatedAt: new Date().toISOString(), }); t.delete(draftDocument._id); } await t.commit(); toast.push({ title: 'Publish Successful', description: `${selectedIds.size} documents published`, status: 'success', closable: true, duration: 10 * 1000, }); setSelectedIds(new Set()); } catch (e) { handleBulkOperationError(e, 'publish', Array.from(selectedIds)); toast.push({ title: 'Error Bulk Publishing', description: ( <>

The bulk publish failed.

), status: 'error', closable: true, duration: 30 * 1000, }); } finally { setDialogMode(null); setLoading(false); } }; const handleDelete = async () => { setLoading(true); try { const idsToDelete = await client.fetch('*[_id in $ids]._id', { ids: Array.from(selectedIds) .map((id) => [id, `drafts.${id}`]) .flat(), }); const t = client.transaction(); for (const id of idsToDelete) { t.delete(id); } await t.commit(); toast.push({ title: 'Delete Successful', description: `${selectedIds.size} documents deleted`, status: 'success', closable: true, duration: 10 * 1000, }); onDelete(); } catch (e) { handleBulkOperationError(e, 'delete', Array.from(selectedIds)); toast.push({ title: 'Error Bulk Deleting', description: ( <>

The bulk delete failed. This usually occurs because there are other documents referencing the documents you’re trying to delete.

), status: 'error', closable: true, duration: 30 * 1000, }); } finally { setDialogMode(null); setLoading(false); } }; return ( <> } popover={{ portal: true, placement: 'bottom' }} menu={ setDialogMode('publish')} /> setDialogMode('discard_changes')} /> setDialogMode('unpublish')} /> setDialogMode('delete')} /> } /> {dialogMode === 'discard_changes' && ( )} {dialogMode === 'unpublish' && ( )} {dialogMode === 'publish' && ( )} {dialogMode === 'delete' && ( )} ); } export default BulkActionsMenu;