Ctrl K
ComunidadGuías…

src/components/tasks/subject-picker.tsx

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

Archivo original ↗Preguntar a KiBot
"use client";

import { useState } from "react";
import {
  BriefcaseIcon,
  Building2Icon,
  CircleUserRoundIcon,
  FileTextIcon,
  StickyNoteIcon,
  UserRoundIcon,
  XIcon,
} from "lucide-react";
import { Button } from "@/components/ui/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import {
  Command,
  CommandGroup,
  CommandInput,
  CommandItem,
  CommandList,
  CommandSeparator,
} from "@/components/ui/command";
import { SUBJECT_LABEL, type TaskSubjectKind } from "@/lib/task-subject";
import { cn } from "@/lib/utils";

/**
 * What a task is about, picked from everything it could be about.
 *
 * One list across six kinds rather than a kind selector and then a record
 * selector. Nobody thinks "a contact task about Priya"; they think "about
 * Priya" — so you type Priya and the row says which kind she is. The kinds are
 * headings, and the search runs across all of them at once.
 *
 * Sending the choice up as `{kind, id}` rather than five nullable props keeps
 * the "at most one" rule in one place instead of in every caller.
 */
export type SubjectOption = {
  kind: TaskSubjectKind;
  id: string;
  label: string;
  /** Second line, where the label alone would be ambiguous. */
  hint?: string;
};

const ICON: Record<TaskSubjectKind, typeof BriefcaseIcon> = {
  application: BriefcaseIcon,
  company: Building2Icon,
  contact: CircleUserRoundIcon,
  resume: FileTextIcon,
  role: UserRoundIcon,
  note: StickyNoteIcon,
};

/** The order the groups appear in. Most-attached first. */
const ORDER: TaskSubjectKind[] = ["application", "contact", "company", "resume", "role", "note"];

export function SubjectPicker({
  value,
  options,
  onChange,
  className,
  size = "default",
}: {
  value: { kind: TaskSubjectKind; id: string } | null;
  options: SubjectOption[];
  onChange: (next: { kind: TaskSubjectKind; id: string } | null) => void;
  className?: string;
  size?: "default" | "sm";
}) {
  const [open, setOpen] = useState(false);
  const chosen = value
    ? (options.find((option) => option.kind === value.kind && option.id === value.id) ?? null)
    : null;
  const Icon = chosen ? ICON[chosen.kind] : null;

  const groups = ORDER.map((kind) => ({
    kind,
    rows: options.filter((option) => option.kind === kind),
  })).filter((group) => group.rows.length > 0);

  return (
    <Popover open={open} onOpenChange={setOpen}>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          role="combobox"
          aria-expanded={open}
          aria-label="What this task is about"
          className={cn(
            "min-w-0 justify-start gap-2 font-normal",
            size === "sm" ? "h-8 px-2 text-[12px]" : "h-9 px-3",
            !chosen && "text-muted-foreground",
            className,
          )}
        >
          {Icon ? <Icon className="size-3.5 shrink-0 opacity-70" /> : null}
          <span className="truncate">{chosen ? chosen.label : "About nothing"}</span>
        </Button>
      </PopoverTrigger>
      <PopoverContent align="start" className="w-72 p-0">
        <Command loop>
          <CommandInput placeholder="A job, a person, a company…" className="h-9" />
          <CommandList className="max-h-72">
            <CommandGroup>
              <CommandItem
                value="about nothing"
                onSelect={() => {
                  onChange(null);
                  setOpen(false);
                }}
                className="text-muted-foreground px-2 py-1.5"
              >
                <XIcon className="size-3.5" />
                About nothing
              </CommandItem>
            </CommandGroup>
            {groups.map((group) => {
              const GroupIcon = ICON[group.kind];
              return (
                <div key={group.kind}>
                  <CommandSeparator />
                  <CommandGroup heading={SUBJECT_LABEL[group.kind]}>
                    {group.rows.map((option) => (
                      <CommandItem
                        // Kind in the value as well as the id: two kinds can
                        // hold the same name, and cmdk keys selection on it.
                        key={`${option.kind}:${option.id}`}
                        value={`${option.label} ${option.hint ?? ""} ${option.kind} ${option.id}`}
                        onSelect={() => {
                          onChange({ kind: option.kind, id: option.id });
                          setOpen(false);
                        }}
                        className="px-2 py-1.5"
                      >
                        <GroupIcon className="size-3.5 shrink-0 opacity-70" />
                        <span className="min-w-0 flex-1 truncate">
                          {option.label}
                          {option.hint && (
                            <span className="text-faint"> · {option.hint}</span>
                          )}
                        </span>
                      </CommandItem>
                    ))}
                  </CommandGroup>
                </div>
              );
            })}
          </CommandList>
        </Command>
      </PopoverContent>
    </Popover>
  );
}

/** The five nullable ids a server action wants, from the one thing you picked. */
export function subjectColumns(value: { kind: TaskSubjectKind; id: string } | null) {
  return {
    applicationId: value?.kind === "application" ? value.id : null,
    companyId: value?.kind === "company" ? value.id : null,
    contactId: value?.kind === "contact" ? value.id : null,
    resumeId: value?.kind === "resume" ? value.id : null,
    roleId: value?.kind === "role" ? value.id : null,
    noteId: value?.kind === "note" ? value.id : null,
  };
}