diff --git a/src/components/WellShow/AssetPreviewWithOverlay.tsx b/src/components/WellShow/AssetPreviewWithOverlay.tsx index f6e8404a..c73eeabc 100644 --- a/src/components/WellShow/AssetPreviewWithOverlay.tsx +++ b/src/components/WellShow/AssetPreviewWithOverlay.tsx @@ -1,9 +1,43 @@ -import { Box, Typography, Button } from '@mui/material' -import type { IAsset } from '@/interfaces/ocotillo' +import { useState } from 'react' +import { Autocomplete, Box, Typography, Button, TextField } from '@mui/material' +import type { IAsset, IWell } from '@/interfaces/ocotillo' import { QueryObserverResult } from '@tanstack/react-query' -import { GetListResponse, HttpError } from '@refinedev/core' +import { + GetListResponse, + HttpError, + useCustomMutation, + useNotification, +} from '@refinedev/core' +import { useAutocomplete } from '@refinedev/mui' import { HttpStatus } from '@/enums' import { isImage, isPdf, isText } from '@/utils' +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from '@/components/ui/alert-dialog' +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' +import { Button as UiButton } from '@/components/ui/button' +import { Link2, MoreVertical, Trash2, Unlink } from 'lucide-react' const previewStyles = { grid: { @@ -78,14 +112,38 @@ export const AssetPreviewWithOverlay = ({ asset, variant, refetchAssets, + canManageAsset = false, }: { asset: IAsset variant: 'grid' | 'slideshow' refetchAssets: () => Promise< QueryObserverResult, HttpError> > + canManageAsset?: boolean }) => { const isSlideshow = variant === 'slideshow' + const { open: notify } = useNotification() + const { mutateAsync: mutateAsset, mutation: assetMutation } = + useCustomMutation() + const [confirmAction, setConfirmAction] = useState< + 'disassociate-asset' | 'delete-asset' | null + >(null) + const [isReassociateDialogOpen, setIsReassociateDialogOpen] = useState(false) + const [selectedWell, setSelectedWell] = useState(null) + const { autocompleteProps: wellAutocompleteProps } = useAutocomplete({ + resource: 'thing/water-well', + dataProviderName: 'ocotillo', + queryOptions: { + enabled: canManageAsset && isReassociateDialogOpen, + }, + onSearch: (value) => [ + { + field: 'name', + operator: 'contains', + value, + }, + ], + }) const getRefreshedAsset = async ( assetId: IAsset['id'], @@ -151,62 +209,160 @@ export const AssetPreviewWithOverlay = ({ } } - return ( - - + const getMutationErrorMessage = (error: unknown) => { + if ( + typeof error === 'object' && + error !== null && + 'response' in error && + typeof error.response === 'object' && + error.response !== null && + 'data' in error.response + ) { + const responseData = error.response.data as { detail?: string } - + if (typeof responseData.detail === 'string') { + return responseData.detail + } + } + + if (error instanceof Error && error.message) { + return error.message + } + + return 'Request failed. Please try again.' + } + + const handleConfirmAssetAction = async () => { + if (!confirmAction) return - { + if (!selectedWell) return + + try { + await mutateAsset({ + url: `asset/${asset.id}/association`, + method: 'patch', + values: { thing_id: selectedWell.id }, + dataProviderName: 'ocotillo', + }) + + await refetchAssets() + notify?.({ + type: 'success', + message: 'Attachment reassociated', + description: selectedWell.name + ? `Attachment moved to ${selectedWell.name}.` + : undefined, + }) + setIsReassociateDialogOpen(false) + setSelectedWell(null) + } catch (error) { + console.error(error) + notify?.({ + type: 'error', + message: 'Could not reassociate attachment', + description: getMutationErrorMessage(error), + }) + } + } + + const currentThingId = asset.thing_id ?? null + const wellOptions = ((wellAutocompleteProps.options ?? []) as IWell[]).filter( + (well) => well.id !== currentThingId + ) + + return ( + <> + - {asset.name} - + - {asset.signed_url && ( - - )} - + {asset.signed_url && ( + + )} + + {canManageAsset && ( + + + event.stopPropagation()} + disabled={assetMutation.isPending} + className="bg-background hover:bg-background" + > + + + + + { + event.stopPropagation() + setConfirmAction('disassociate-asset') + }} + > + + Disassociate attachment + + { + event.stopPropagation() + setSelectedWell(null) + setIsReassociateDialogOpen(true) + }} + > + + Reassociate attachment + + + { + event.stopPropagation() + setConfirmAction('delete-asset') + }} + > + + Delete attachment + + + + )} + + + + { + if (!open && !assetMutation.isPending) { + setConfirmAction(null) + } + }} + > + + + + {confirmAction === 'disassociate-asset' + ? 'Disassociate this attachment?' + : 'Delete this attachment?'} + + + {confirmAction === 'disassociate-asset' + ? 'The asset will remain uploaded, but it will no longer be associated with any well.' + : 'This permanently deletes the uploaded asset record. Use this only when the file should not be kept.'} + + + + + Cancel + + { + event.preventDefault() + void handleConfirmAssetAction() + }} + disabled={assetMutation.isPending} + className={ + confirmAction === 'delete-asset' + ? 'bg-destructive text-destructive-foreground hover:bg-destructive/90' + : undefined + } + > + {assetMutation.isPending + ? 'Working...' + : confirmAction === 'disassociate-asset' + ? 'Disassociate attachment' + : 'Delete attachment'} + + + + + + { + if (!open && assetMutation.isPending) { + return + } + + setIsReassociateDialogOpen(open) + + if (!open) { + setSelectedWell(null) + } + }} + > + + + Reassociate attachment + + Move this attachment to one other well. It will be removed from + any current well association. + + + + + options} + getOptionLabel={(well) => well?.name ?? ''} + isOptionEqualToValue={(option, value) => option.id === value.id} + onChange={(_, value) => setSelectedWell(value)} + renderOption={(props, well) => ( + + {well.name} + + )} + renderInput={(params) => ( + + )} + /> + + + + setIsReassociateDialogOpen(false)} + disabled={assetMutation.isPending} + > + Cancel + + void handleReassociateAsset()} + disabled={!selectedWell || assetMutation.isPending} + > + {assetMutation.isPending ? 'Working...' : 'Reassociate'} + + + + + ) } diff --git a/src/components/WellShow/Attachments.tsx b/src/components/WellShow/Attachments.tsx index 976bdaf1..2981391f 100644 --- a/src/components/WellShow/Attachments.tsx +++ b/src/components/WellShow/Attachments.tsx @@ -1,7 +1,6 @@ import { useMemo, useState } from 'react' import { Box, - ButtonBase, IconButton, Stack, Typography, @@ -65,6 +64,11 @@ export const AttachmentsCard = ({ const { canManageAmp } = useAccessCapabilities() + const openSlideshow = (index: number) => { + setSlideshowIndex(index) + setPreviewViewMode('slideshow') + } + return ( {previewAssets.map((asset, idx) => ( - { - setSlideshowIndex(idx) - setPreviewViewMode('slideshow') + onClick={(event) => { + const target = event.target + + if ( + target instanceof Node && + !event.currentTarget.contains(target) + ) { + return + } + + openSlideshow(idx) + }} + onKeyDown={(event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault() + openSlideshow(idx) + } }} sx={{ display: 'block', @@ -157,14 +176,23 @@ export const AttachmentsCard = ({ overflow: 'hidden', boxShadow: 2, textAlign: 'left', + cursor: 'pointer', + outline: 'none', + '&:focus-visible': { + boxShadow: 4, + outline: '2px solid', + outlineColor: 'primary.main', + outlineOffset: 2, + }, }} > - + ))} ) : ( @@ -186,6 +214,7 @@ export const AttachmentsCard = ({ asset={currentAsset} variant="slideshow" refetchAssets={refetchAssets} + canManageAsset={canManageAmp} /> )} {previewAssets.length > 1 && (