All files / shared / components CrudModal.tsx

59.57% Statements 28/47
57.14% Branches 24/42
55.55% Functions 10/18
63.63% Lines 28/44

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91          46x   46x   714x 714x                                 714x               113x     10x 7x   3x         2x 2x                 113x       15x   2x 2x                                           2x          
import { useState, type ReactNode } from 'react'
import {
  Dialog,
  DialogContent,
  DialogHeader,
  DialogTitle,
  DialogFooter,
} from '@/components/ui/dialog'
import { Button } from '@/components/ui/button'
import { DiscardDialog } from './DiscardDialog'
import { Loader2 } from 'lucide-react'

export interface CrudModalProps {
  open: boolean
  title: string
  isDirty: boolean
  isPending?: boolean
  saveLabel?: string
  cancelLabel?: string
  onClose: () => void
  onSave?: () => void
  children: ReactNode
}

export function CrudModal({
  open,
  title,
  isDirty,
  isPending = false,
  saveLabel = 'Save',
  cancelLabel = 'Cancel',
  onClose,
  onSave,
  children,
}: CrudModalProps) {
  const [showDiscard, setShowDiscard] = useState(false)
 
  function handleCancel() {
    if (isDirty && !isPending) {
      setShowDiscard(true)
    } else {
      onClose()
    }
  }
 
  function handleDiscard() {
    setShowDiscard(false)
    onClose()
  }
 
  function handleOpenChange(open: boolean) {
    if (!open) {
      handleCancel()
    }
  }
 
  return (
    <Dialog open={open} onOpenChange={handleOpenChange}>
      <DialogContent
        showCloseButton={false}
        onInteractOutside={(e) => e.preventDefault()}
        onEscapeKeyDown={(e) => {
          e.preventDefault()
          handleCancel()
        }}
      >
        <DialogHeader className="mb-3">
          <DialogTitle className="text-lg font-semibold">{title}</DialogTitle>
        </DialogHeader>
        <div className="mb-3">{children}</div>
        <DialogFooter className="flex justify-end gap-2">
          <Button variant="outline" onClick={handleCancel} type="button" disabled={isPending}>
            {cancelLabel}
          </Button>
          {onSave && (
            <Button onClick={onSave} disabled={isPending} type="button">
              {isPending && <Loader2 className="mr-1.5 inline-block size-4 animate-spin" />}
              {isPending ? 'Saving...' : saveLabel}
            </Button>
          )}
        </DialogFooter>
      </DialogContent>
      <DiscardDialog
        open={showDiscard}
        onDiscard={handleDiscard}
        onKeepEditing={() => setShowDiscard(false)}
      />
    </Dialog>
  )
}