Ctrl K
ComunidadGuías…

src/components/dives/dive-import-dialog.tsx

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
"use client";

import { useRef, useState } from "react";
import { useForm } from "react-hook-form";
import { Save } from "lucide-react";

import { divesAPI, type Dive } from "@/lib/api/dives";
import { getApiErrorMessage } from "@/lib/api/error";
import {
  buildDiveUpdate,
  diveToFormValues,
  diveUpdateSchema,
  type DiveUpdateInput,
} from "@/lib/validations/dive";
import { DiveFileImport } from "@/components/dives/dive-file-import";
import { diveTitleText } from "@/components/dives/dive-title";
import { useMixtureFieldArray } from "@/components/dives/mixture-fields";
import { usePendingDiveFiles } from "@/components/dives/use-pending-dive-files";
import { Button } from "@/components/ui/button";
import { ButtonSpinner } from "@/components/ui/button-spinner";
import {
  Dialog,
  DialogContent,
  DialogDescription,
  DialogFooter,
  DialogHeader,
  DialogTitle,
} from "@/components/ui/dialog";
import { FormApiError } from "@/components/ui/form-api-error";
import { useToast } from "@/components/ui/use-toast";

export interface DiveImportDialogProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  dive: Dive;
  /** Called once the dive has saved, so the page can re-read it. */
  onSaved: () => void;
}

/**
 * The dive form's Import section in a dialog, over the Recordings card: files
 * picked here and files struck off go out on Save, as on the Edit page, and
 * Cancel leaves the dive's files as it found them.
 */
export function DiveImportDialog({
  open,
  onOpenChange,
  dive,
  onSaved,
}: DiveImportDialogProps) {
  const panel = useRef<HTMLDivElement>(null);

  return (
    <Dialog open={open} onOpenChange={onOpenChange}>
      <DialogContent
        ref={panel}
        className="sm:max-w-2xl"
        // The panel rather than the upload button - see `DiveSectionDialog`.
        initialFocus={panel}
      >
        <DialogHeader>
          <DialogTitle>Edit recordings</DialogTitle>
          <DialogDescription>
            {diveTitleText(dive.dive_number, dive.dive_sites)}
          </DialogDescription>
        </DialogHeader>
        <DiveImportForm
          dive={dive}
          onCancel={() => onOpenChange(false)}
          onSaved={() => {
            onSaved();
            onOpenChange(false);
          }}
        />
      </DialogContent>
    </Dialog>
  );
}

function DiveImportForm({
  dive,
  onCancel,
  onSaved,
}: {
  dive: Dive;
  onCancel: () => void;
  onSaved: () => void;
}) {
  const { toast } = useToast();
  const recordings = dive.recordings ?? [];
  const files = usePendingDiveFiles(recordings);
  // The whole dive, which a picked file fills the blanks of. None of those fields
  // is on screen here, so only the ones a file wrote are validated and sent: a
  // stored value this dialog does not show can neither block the save nor ride
  // along with it.
  const form = useForm<DiveUpdateInput>({
    defaultValues: diveToFormValues(dive),
  });
  const mixtureFieldArray = useMixtureFieldArray(form.control);
  const [written, setWritten] = useState<ReadonlySet<string>>(new Set());
  const [isSubmitting, setIsSubmitting] = useState(false);
  const [apiError, setApiError] = useState<string | null>(null);

  const save = async () => {
    setApiError(null);
    const keys = [...written].filter((key) => key in diveUpdateSchema.shape);
    let update = {};
    if (keys.length > 0) {
      const parsed = diveUpdateSchema
        .pick(
          Object.fromEntries(keys.map((key) => [key, true])) as Partial<
            Record<keyof typeof diveUpdateSchema.shape, true>
          >,
        )
        .safeParse(form.getValues());
      if (!parsed.success) {
        setApiError(
          parsed.error.issues[0]?.message ??
            "The file filled in a value the dive cannot hold.",
        );
        return;
      }
      update = buildDiveUpdate(parsed.data as DiveUpdateInput);
    }

    setIsSubmitting(true);
    try {
      if (Object.keys(update).length > 0) {
        await divesAPI.updateDive(dive.uuid, update);
      }
    } catch (error) {
      setApiError(
        getApiErrorMessage(error, "Failed to update dive. Please try again."),
      );
      setIsSubmitting(false);
      return;
    }

    await files.save(dive.uuid);

    setIsSubmitting(false);
    toast({ title: "Success", description: "Dive updated successfully!" });
    onSaved();
  };

  return (
    <div className="space-y-6">
      <DiveFileImport
        bare
        form={form}
        replaceMixtures={mixtureFieldArray.replace}
        {...files.fileList}
        recordings={recordings}
        diveUuid={dive.uuid}
        returnTo={`/dives/${dive.uuid}`}
        onWrite={(path) =>
          setWritten((paths) =>
            paths.has(path) ? paths : new Set(paths).add(path),
          )
        }
      />

      <FormApiError error={apiError} />

      <DialogFooter>
        <Button type="button" variant="outline" onClick={onCancel}>
          Cancel
        </Button>
        <Button
          type="button"
          onClick={save}
          disabled={isSubmitting || !files.hasChanges}
        >
          {isSubmitting ? (
            <>
              <ButtonSpinner className="mr-2" />
              Saving...
            </>
          ) : (
            <>
              <Save className="h-4 w-4 mr-2" />
              Save changes
            </>
          )}
        </Button>
      </DialogFooter>
    </div>
  );
}