diff --git a/src/app/volunteers/instructions/page.tsx b/src/app/volunteers/instructions/page.tsx index 6dd5e019..db66d07c 100644 --- a/src/app/volunteers/instructions/page.tsx +++ b/src/app/volunteers/instructions/page.tsx @@ -57,8 +57,8 @@ export default async function VolunteersInstructionsPage(): Promise
  • - Double-check notes and role/cohort tags before confirming - save. + Double-check notes and tags (training, position, committee, + language) before confirming save.
  • diff --git a/src/components/settings/ManageTagsContent.tsx b/src/components/settings/ManageTagsContent.tsx index d2015c11..44747777 100644 --- a/src/components/settings/ManageTagsContent.tsx +++ b/src/components/settings/ManageTagsContent.tsx @@ -2,103 +2,67 @@ import React, { useEffect, useState, useTransition } from "react"; import { useRouter } from "next/navigation"; -import { Calendar, ChevronDown, Plus, Tag, Trash2, User } from "lucide-react"; +import { + Briefcase, + ChevronDown, + GraduationCap, + Languages, + Plus, + Tag, + Trash2, + Users, +} from "lucide-react"; import clsx from "clsx"; import toast from "react-hot-toast"; -import type { CohortRow, RoleRow } from "@/components/volunteers/types"; +import type { RoleRow } from "@/components/volunteers/types"; import { - createCohortTagAction, createRoleTagAction, - removeAllCohortTagsAction, - removeAllRoleTagsAction, - removeCohortTagAction, removeRoleTagAction, - updateCohortTagAction, updateRoleTagAction, } from "@/lib/api/actions"; +import { + ROLE_TAG_COLUMN_LABEL, + roleTagColumnLabel, + type RoleTagDbType, +} from "@/lib/volunteers/roleTagLabels"; -const ROLE_TYPES = [ - { value: "current", label: "Current" }, - { value: "prior", label: "Prior" }, - { value: "future_interest", label: "Future interest" }, -] as const; - -const COHORT_TERMS = ["Fall", "Spring", "Summer", "Winter"] as const; +const ROLE_TAG_SECTIONS: { + type: RoleTagDbType; + title: string; + subtitle: string; + icon: React.ElementType; +}[] = [ + { + type: "training", + title: "Training", + subtitle: "Free-text tags shown in the Training column", + icon: GraduationCap, + }, + { + type: "prior", + title: "Position", + subtitle: "Tags shown in the Position column", + icon: Briefcase, + }, + { + type: "current", + title: "Committee", + subtitle: "Tags shown in the Committee column", + icon: Users, + }, + { + type: "future_interest", + title: "Language", + subtitle: "Tags shown in the Language column", + icon: Languages, + }, +]; const inputClass = "rounded-lg border border-gray-200/90 bg-white px-2.5 py-2 text-sm text-gray-900 shadow-sm " + "focus:outline-none focus:ring-2 focus:ring-purple-500/30 focus:border-purple-400"; -const selectBase = - "rounded-lg border border-gray-200/90 bg-white py-2 text-sm text-gray-900 shadow-sm " + - "focus:outline-none focus:ring-2 focus:ring-purple-500/30 focus:border-purple-400"; - -/** Kind / Term selects: hide native arrow and show a chevron inset from the right edge. */ -const selectKindClass = selectBase + " appearance-none pl-2.5 pr-8"; - -function KindSelect({ - value, - onChange, - widthClassName, -}: { - value: string; - onChange: (next: string) => void; - /** e.g. `max-w-[11rem] w-full` for table rows */ - widthClassName: string; -}): React.JSX.Element { - return ( -
    - - -
    - ); -} - -function TermSelect({ - value, - onChange, - widthClassName, -}: { - value: string; - onChange: (next: string) => void; - widthClassName: string; -}): React.JSX.Element { - return ( -
    - - -
    - ); -} - -type RoleDraft = { name: string; type: string }; -type CohortDraft = { term: string; year: string }; +type RoleDraft = { name: string }; function CollapsibleSection({ sectionId, @@ -198,7 +162,6 @@ function CollapsibleSection({ interface ManageTagsContentProps { initialRoles: RoleRow[]; - initialCohorts: CohortRow[]; loadError: string | null; /** When set (e.g. from the volunteers table modal), refreshes client data instead of the Next router. */ onRefresh?: () => void; @@ -208,7 +171,6 @@ interface ManageTagsContentProps { export function ManageTagsContent({ initialRoles, - initialCohorts, loadError, onRefresh, embedded = false, @@ -217,47 +179,36 @@ export function ManageTagsContent({ const [isPending, startTransition] = useTransition(); const [roles, setRoles] = useState(initialRoles); - const [cohorts, setCohorts] = useState(initialCohorts); const [roleDrafts, setRoleDrafts] = useState>({}); - const [cohortDrafts, setCohortDrafts] = useState>( - {} - ); - const [newRoleName, setNewRoleName] = useState(""); - const [newRoleType, setNewRoleType] = useState("current"); - const [newCohortTerm, setNewCohortTerm] = - useState<(typeof COHORT_TERMS)[number]>("Fall"); - const [newCohortYear, setNewCohortYear] = useState( - String(new Date().getFullYear()) - ); - - const [openRoles, setOpenRoles] = useState(true); - const [openCohorts, setOpenCohorts] = useState(true); + const [newTagNameByType, setNewTagNameByType] = useState< + Record + >({ + training: "", + prior: "", + current: "", + future_interest: "", + }); + + const [openRoleSection, setOpenRoleSection] = useState< + Record + >({ + training: true, + prior: true, + current: true, + future_interest: true, + }); useEffect(() => { setRoles(initialRoles); }, [initialRoles]); - useEffect(() => { - setCohorts(initialCohorts); - }, [initialCohorts]); - useEffect(() => { setRoleDrafts( - Object.fromEntries( - roles.map((r) => [r.id, { name: r.name, type: r.type }]) - ) + Object.fromEntries(roles.map((r) => [r.id, { name: r.name }])) ); }, [roles]); - useEffect(() => { - setCohortDrafts( - Object.fromEntries( - cohorts.map((c) => [c.id, { term: c.term, year: String(c.year) }]) - ) - ); - }, [cohorts]); - const refresh = (): void => { if (onRefresh) { onRefresh(); @@ -273,17 +224,17 @@ export function ManageTagsContent({ startTransition(async () => { const res = await updateRoleTagAction(id, { name: draft.name, - type: draft.type, + type: row.type, is_active: row.is_active, }); if (res.success) { - toast.success("Role saved"); + toast.success("Tag saved"); refresh(); } else { - toast.error(res.error ?? "Could not save role"); + toast.error(res.error ?? "Could not save tag"); setRoleDrafts((prev) => ({ ...prev, - [id]: { name: row.name, type: row.type }, + [id]: { name: row.name }, })); } }); @@ -294,7 +245,7 @@ export function ManageTagsContent({ if (!row) return; if ( !window.confirm( - `Delete role "${row.name}" (${row.type})? Volunteer links to this role will be removed if the database is set up to cascade.` + `Delete tag "${row.name}" (${roleTagColumnLabel(row.type)})? Volunteer links to this tag will be removed if the database is set up to cascade.` ) ) { return; @@ -302,163 +253,68 @@ export function ManageTagsContent({ startTransition(async () => { const res = await removeRoleTagAction(row.id); if (res.success) { - toast.success("Role removed"); + toast.success("Tag removed"); refresh(); } else { - toast.error(res.error ?? "Could not remove role"); + toast.error(res.error ?? "Could not remove tag"); setRoleDrafts((prev) => ({ ...prev, - [id]: { name: row.name, type: row.type }, + [id]: { name: row.name }, })); } }); }; - const handleCreateRole = (e: React.FormEvent): void => { + const handleCreateRoleForType = ( + e: React.FormEvent, + type: RoleTagDbType + ): void => { e.preventDefault(); - const name = newRoleName.trim(); + const name = newTagNameByType[type].trim(); if (!name) { - toast.error("Enter a role name"); + toast.error("Enter a tag name"); return; } startTransition(async () => { const res = await createRoleTagAction({ name, - type: newRoleType, + type, is_active: true, }); if (res.success) { - toast.success("Role created"); - setNewRoleName(""); - setNewRoleType("current"); + toast.success("Tag created"); + setNewTagNameByType((prev) => ({ ...prev, [type]: "" })); refresh(); } else { - toast.error(res.error ?? "Could not create role"); + toast.error(res.error ?? "Could not create tag"); } }); }; - const handleSaveCohort = (id: number): void => { - const draft = cohortDrafts[id]; - const row = cohorts.find((c) => c.id === id); - if (!draft || !row) return; - const y = parseInt(draft.year.trim(), 10); - if (!Number.isInteger(y) || y < 1900 || y > 2100) { - toast.error("Enter a valid year (1900–2100)"); - return; - } - startTransition(async () => { - const res = await updateCohortTagAction(id, { - term: draft.term, - year: y, - is_active: row.is_active, - }); - if (res.success) { - toast.success("Cohort saved"); - refresh(); - } else { - toast.error(res.error ?? "Could not save cohort"); - setCohortDrafts((prev) => ({ - ...prev, - [id]: { term: row.term, year: String(row.year) }, - })); - } - }); - }; - - const handleDeleteCohort = (id: number): void => { - const row = cohorts.find((c) => c.id === id); - if (!row) return; + const handleRemoveAllRolesForType = (type: RoleTagDbType): void => { + const inColumn = roles.filter((r) => r.type === type); + if (inColumn.length === 0) return; + const label = ROLE_TAG_COLUMN_LABEL[type]; if ( !window.confirm( - `Delete cohort "${row.term} ${row.year}"? Volunteer links may be removed if the database cascades deletes.` + `Remove all ${label} tags (${inColumn.length})? This cannot be undone. Links between volunteers and these tags will be removed if the database cascades deletes.` ) ) { return; } startTransition(async () => { - const res = await removeCohortTagAction(row.year, row.term); - if (res.success) { - toast.success("Cohort removed"); - refresh(); - } else { - toast.error(res.error ?? "Could not remove cohort"); - setCohortDrafts((prev) => ({ - ...prev, - [id]: { term: row.term, year: String(row.year) }, - })); - } - }); - }; - - const handleRemoveAllRoles = (): void => { - if (roles.length === 0) return; - if ( - !window.confirm( - "Remove all roles? This cannot be undone. Links between volunteers and these roles will be deleted." - ) - ) { - return; - } - startTransition(async () => { - const res = await removeAllRoleTagsAction(); - if (res.success) { - toast.success( - res.removed === 0 - ? "No roles to remove" - : `Removed ${res.removed} role(s)` - ); - refresh(); - } else { - toast.error(res.error); - } - }); - }; - - const handleRemoveAllCohorts = (): void => { - if (cohorts.length === 0) return; - if ( - !window.confirm( - "Remove all cohorts? This cannot be undone. Links between volunteers and these cohorts will be deleted." - ) - ) { - return; - } - startTransition(async () => { - const res = await removeAllCohortTagsAction(); - if (res.success) { - toast.success( - res.removed === 0 - ? "No cohorts to remove" - : `Removed ${res.removed} cohort(s)` - ); - refresh(); - } else { - toast.error(res.error); - } - }); - }; - - const handleCreateCohort = (e: React.FormEvent): void => { - e.preventDefault(); - const y = parseInt(newCohortYear.trim(), 10); - if (!Number.isInteger(y) || y < 1900 || y > 2100) { - toast.error("Enter a valid year (1900–2100)"); - return; - } - startTransition(async () => { - const res = await createCohortTagAction({ - term: newCohortTerm, - year: y, - is_active: true, - }); - if (res.success) { - toast.success("Cohort created"); - setNewCohortYear(String(new Date().getFullYear())); - refresh(); - } else { - toast.error(res.error ?? "Could not create cohort"); + for (const r of inColumn) { + const res = await removeRoleTagAction(r.id); + if (!res.success) { + toast.error(res.error ?? `Could not remove tag "${r.name}"`); + refresh(); + return; + } } + toast.success( + `Removed ${inColumn.length} ${label} tag${inColumn.length === 1 ? "" : "s"}` + ); + refresh(); }); }; @@ -466,15 +322,7 @@ export function ManageTagsContent({ const row = roles.find((r) => r.id === id); const d = roleDrafts[id]; if (!row || !d) return false; - return row.name !== d.name || row.type !== d.type; - }; - - const cohortDirty = (id: number): boolean => { - const row = cohorts.find((c) => c.id === id); - const d = cohortDrafts[id]; - if (!row || !d) return false; - const y = parseInt(d.year, 10); - return row.term !== d.term || row.year !== y; + return row.name !== d.name; }; if (loadError) { @@ -500,323 +348,163 @@ export function ManageTagsContent({ ) : null} - setOpenRoles((o) => !o)} - embedded={embedded} - > -
    - - - - - - - - - - {roles.length === 0 ? ( - - - - ) : ( - roles.map((r) => { - const d = roleDrafts[r.id]; - if (!d) return null; - return ( - - - - - - ); - }) - )} - -
    NameKind - Actions -
    -
    - - - -

    - No roles yet -

    -

    - Add a role below — it will appear in filters and when - editing volunteers. -

    -
    -
    - - setRoleDrafts((prev) => ({ - ...prev, - [r.id]: { ...d, name: e.target.value }, - })) - } - /> - - - setRoleDrafts((prev) => ({ - ...prev, - [r.id]: { ...d, type: next }, - })) - } - widthClassName="w-full max-w-[11rem] min-w-0" - /> - -
    - - -
    -
    -
    - -
    -

    - - Add a role -

    -
    -
    - - setNewRoleName(e.target.value)} - placeholder="e.g. Greeter" - /> -
    -
    - - -
    - -
    -
    - -
    - -
    -
    - - setOpenCohorts((o) => !o)} - embedded={embedded} - > -
    - - - - - - - - - - {cohorts.length === 0 ? ( - - - - ) : ( - cohorts.map((c) => { - const d = cohortDrafts[c.id]; - if (!d) return null; - return ( - - - - + + + + ); + }) + )} + +
    TermYear - Actions -
    -
    - - - -

    - No cohorts yet -

    -

    - Create a cohort below to use it when linking volunteers. -

    -
    -
    - - setCohortDrafts((prev) => ({ - ...prev, - [c.id]: { ...d, term: next }, - })) - } - widthClassName="w-full max-w-[9rem] min-w-0" - /> - - - setCohortDrafts((prev) => ({ - ...prev, - [c.id]: { ...d, year: e.target.value }, - })) - } - /> - -
    - - +
    + + + + + + + + + {columnRoles.length === 0 ? ( + + - ); - }) - )} - -
    Name + Actions +
    +
    + + + +

    + No {title.toLowerCase()} tags yet +

    +

    + Add a tag below — it will appear in filters and when + editing volunteers. +

    -
    - -
    -

    - - Add a cohort -

    -
    -
    - - - setNewCohortTerm(next as (typeof COHORT_TERMS)[number]) - } - widthClassName="w-full" - /> + ) : ( + columnRoles.map((r) => { + const d = roleDrafts[r.id]; + if (!d) return null; + return ( +
    + + setRoleDrafts((prev) => ({ + ...prev, + [r.id]: { name: e.target.value }, + })) + } + /> + +
    + + +
    +
    -
    - - setNewCohortYear(e.target.value)} - /> + +
    +

    + + Add {title.toLowerCase()} tag +

    + handleCreateRoleForType(e, type)} + className="flex flex-col gap-3 sm:flex-row sm:flex-wrap sm:items-end" + > +
    + + + setNewTagNameByType((prev) => ({ + ...prev, + [type]: e.target.value, + })) + } + placeholder={"Type here"} + /> +
    + +
    - - -
    -
    - -
    -
    +
    + +
    + + ); + })} ); } diff --git a/src/components/volunteers/AddVolunteerModal.tsx b/src/components/volunteers/AddVolunteerModal.tsx index af9eb0a4..271e56ec 100644 --- a/src/components/volunteers/AddVolunteerModal.tsx +++ b/src/components/volunteers/AddVolunteerModal.tsx @@ -4,14 +4,11 @@ import React, { useState, useEffect, useCallback, useId } from "react"; import { X, UserPlus } from "lucide-react"; import clsx from "clsx"; import { createVolunteerAction } from "@/lib/api/actions"; -import type { CohortTerm } from "@/lib/api/createVolunteer"; import { NEW_VOLUNTEER_FORM_COLUMNS } from "./volunteerColumns"; import type { Volunteer } from "./types"; import { VolunteerTag } from "./VolunteerTag"; import { OPT_IN_OPTIONS } from "./utils"; -const COHORT_TERMS: CohortTerm[] = ["Fall", "Winter", "Spring", "Summer"]; - const inputClass = "w-full rounded-lg border border-gray-200 bg-white px-3 py-2.5 text-sm text-gray-900 placeholder:text-gray-400 shadow-sm transition " + "focus:outline-none focus:ring-2 focus:ring-purple-500/25 focus:border-purple-500"; @@ -39,11 +36,13 @@ const FORM_SECTIONS: { columnIds: ["name_org", "pseudonym", "pronouns", "email", "phone"], }, { - title: "Cohorts", + title: "Training", + description: + "Add any text labels (same idea as Position). Suggestions come from tags your admins created.", columnIds: ["cohorts"], }, { - title: "Roles", + title: "Position, committee, and language", columnIds: ["prior_roles", "current_roles", "future_interests"], }, { @@ -52,20 +51,6 @@ const FORM_SECTIONS: { }, ]; -type CohortFormRow = { term: CohortTerm; year: string }; - -function parseCohortLabel( - label: string -): { term: CohortTerm; year: string } | null { - const parts = label.trim().split(/\s+/); - if (parts.length < 2) return null; - const year = parts[parts.length - 1]!; - const term = parts.slice(0, -1).join(" "); - if (!/^\d{4}$/.test(year)) return null; - if (!COHORT_TERMS.includes(term as CohortTerm)) return null; - return { term: term as CohortTerm, year }; -} - function SectionCard({ title, description, @@ -163,7 +148,7 @@ function MultiRoleField({ type="text" className="flex-1 min-w-40 border-0 bg-transparent py-1 px-1 text-sm outline-none placeholder:text-gray-400" placeholder={ - values.length > 0 ? "Add another…" : "Type a role, then press Enter" + values.length > 0 ? "Add another…" : "Type a tag, then press Enter" } value={draft} list={suggestionOptions.length > 0 ? datalistId : undefined} @@ -187,138 +172,12 @@ function MultiRoleField({ ); } -function CohortField({ - label, - icon: Icon, - cohortLabels, - cohortRows, - setCohortRows, - currentYear, -}: { - label: string; - icon: React.ElementType; - cohortLabels: string[]; - cohortRows: CohortFormRow[]; - setCohortRows: React.Dispatch>; - currentYear: number; -}): React.JSX.Element { - const [term, setTerm] = useState("Fall"); - const [year, setYear] = useState(String(currentYear)); - - const addDraft = useCallback((): void => { - const y = parseInt(year.trim(), 10); - if (!Number.isInteger(y) || y < 1900 || y > 2100) return; - const yStr = String(y); - setCohortRows((rows) => { - if (rows.some((r) => r.term === term && r.year === yStr)) return rows; - return [...rows, { term, year: yStr }]; - }); - }, [term, year, setCohortRows]); - - const applyPreset = useCallback( - (presetLabel: string): void => { - const parsed = parseCohortLabel(presetLabel); - if (!parsed) return; - setCohortRows((rows) => { - if ( - rows.some((r) => r.term === parsed.term && r.year === parsed.year) - ) { - return rows; - } - return [...rows, parsed]; - }); - }, - [setCohortRows] - ); - - const removeAt = (index: number): void => { - setCohortRows((rows) => rows.filter((_, i) => i !== index)); - }; - - return ( -
    - {label} - -
    - {cohortRows.map((row, i) => ( - removeAt(i)} - /> - ))} -
    - - setYear(e.target.value)} - onKeyDown={(e) => { - if (e.key === "Enter") { - e.preventDefault(); - addDraft(); - } - }} - className="w-16 shrink-0 rounded-md border border-gray-200 bg-gray-50/80 py-1.5 px-1.5 text-center text-sm tabular-nums outline-none focus:border-purple-400 focus:ring-1 focus:ring-purple-500/30" - aria-label="Cohort year" - /> - -
    -
    - - {cohortLabels.length > 0 ? ( -
    -

    Existing tags:

    -
    - {cohortLabels.map((presetLabel) => ( - - ))} -
    -
    - ) : null} -
    - ); -} - export const AddVolunteerModal = ({ isOpen, onClose, onSuccess, optionsData = {}, }: AddVolunteerModalProps): React.JSX.Element | null => { - const currentYear = new Date().getFullYear(); - const [nameOrg, setNameOrg] = useState(""); const [email, setEmail] = useState(""); const [phone, setPhone] = useState(""); @@ -329,7 +188,7 @@ export const AddVolunteerModal = ({ const [currentRoles, setCurrentRoles] = useState([]); const [priorRoles, setPriorRoles] = useState([]); const [futureRoles, setFutureRoles] = useState([]); - const [cohortRows, setCohortRows] = useState([]); + const [trainingRoles, setTrainingRoles] = useState([]); const [submitting, setSubmitting] = useState(false); const [error, setError] = useState(null); @@ -338,7 +197,7 @@ export const AddVolunteerModal = ({ const currentRoleOptions = optionsData["current_roles"] ?? []; const priorRoleOptions = optionsData["prior_roles"] ?? []; const futureRoleOptions = optionsData["future_interests"] ?? []; - const cohortLabels = optionsData["cohorts"] ?? []; + const trainingTagOptions = optionsData["cohorts"] ?? []; const resetForm = useCallback((): void => { setNameOrg(""); @@ -351,7 +210,7 @@ export const AddVolunteerModal = ({ setCurrentRoles([]); setPriorRoles([]); setFutureRoles([]); - setCohortRows([]); + setTrainingRoles([]); setError(null); }, []); @@ -373,21 +232,11 @@ export const AddVolunteerModal = ({ e.preventDefault(); setError(null); - for (let i = 0; i < cohortRows.length; i++) { - const row = cohortRows[i]!; - const y = parseInt(row.year, 10); - if (!Number.isInteger(y) || y < 1900 || y > 2100) { - setError(`Cohort row ${i + 1}: enter a valid year (1900–2100).`); - return; - } - } - - const cohorts = cohortRows.map((row) => ({ - year: parseInt(row.year, 10), - term: row.term, - })); - const roles = [ + ...trainingRoles.map((name) => ({ + name: name.trim(), + type: "training" as const, + })), ...currentRoles.map((name) => ({ name: name.trim(), type: "current" as const, @@ -415,7 +264,7 @@ export const AddVolunteerModal = ({ notes: notes.trim() || null, }, roles, - cohorts, + cohorts: [], }); if (result.success) { @@ -513,14 +362,13 @@ export const AddVolunteerModal = ({ ); case "cohorts": return ( - ); case "prior_roles": @@ -623,8 +471,8 @@ export const AddVolunteerModal = ({

    - Add multiple cohorts and roles if you need them. You can add - more later and edit the volunteer later. + Add multiple training terms and tags if you need them. You can + add more later by editing the volunteer.

    diff --git a/src/components/volunteers/EditableCell.tsx b/src/components/volunteers/EditableCell.tsx index bf9b7748..32c96bd3 100644 --- a/src/components/volunteers/EditableCell.tsx +++ b/src/components/volunteers/EditableCell.tsx @@ -36,6 +36,8 @@ export const EditableCell = ({ const [isNotesExpanded, setIsNotesExpanded] = useState(false); const [value, setValue] = useState(initialValue); const [inputValue, setInputValue] = useState(""); + /** Newline-separated draft for text columns that store string[] (one line per value). */ + const [multiLineDraft, setMultiLineDraft] = useState(""); const [modalCoords, setModalCoords] = useState<{ top: number; @@ -59,6 +61,8 @@ export const EditableCell = ({ "future_interests", ].includes(info.column.id); + const isMultiLineText = type === "text" && isMulti; + /** Forces a fresh DOM subtree when toggling edit mode; avoids ghost nodes from contentEditable + imperative textContent. */ const modeKey = isEditing ? "edit" : "view"; @@ -87,8 +91,15 @@ export const EditableCell = ({ setInputValue(""); if (type === "text") { - const text = String(value ?? ""); - draftTextRef.current = text; + if (isMultiLineText) { + const arr = Array.isArray(value) ? (value as string[]) : []; + const joined = arr.join("\n"); + draftTextRef.current = joined; + setMultiLineDraft(joined); + } else { + const text = String(value ?? ""); + draftTextRef.current = text; + } } setIsEditing(true); }; @@ -151,6 +162,7 @@ export const EditableCell = ({ useEffect(() => { if (!isEditing || type !== "text") return; + if (isNotes || isMultiLineText) return; const editor = inlineEditorRef.current; if (!editor) return; editor.textContent = draftTextRef.current; @@ -163,13 +175,21 @@ export const EditableCell = ({ range.collapse(false); selection.removeAllRanges(); selection.addRange(range); - }, [isEditing, type]); + }, [isEditing, type, isNotes, isMultiLineText]); const handleDelete = useCallback((): void => { - const cleared = isMulti ? [] : type === "options" ? null : ""; + const cleared = + isMulti || isMultiLineText ? [] : type === "options" ? null : ""; setValue(cleared); onEdit(info.row.original.id, info.column.id, cleared); - }, [isMulti, type, info.row.original.id, info.column.id, onEdit]); + }, [ + isMulti, + isMultiLineText, + type, + info.row.original.id, + info.column.id, + onEdit, + ]); const applyValue = (val: string): void => { if (isMulti) { @@ -202,7 +222,7 @@ export const EditableCell = ({ applyValue(newTag); if (!alreadyExists) { const toastId = `new-tag-${info.column.id}-${newTag.toLowerCase()}`; - toast(`New tag "${newTag}" created — remember to save your changes.`, { + toast(`"${newTag}" added — remember to save your changes.`, { id: toastId, icon: , duration: 4000, @@ -263,8 +283,8 @@ export const EditableCell = ({ placeholder={ allowAdd ? isMulti - ? "Search or create..." - : "Select or create..." + ? "Search or add..." + : "Select or add..." : "Search..." } value={inputValue} @@ -365,6 +385,45 @@ export const EditableCell = ({ ); } + if (isMultiLineText) { + const commitLinesAndExit = (): void => { + const lines = multiLineDraft + .split("\n") + .map((s) => s.trim()) + .filter((s) => s.length > 0); + setValue(lines); + handleSave(lines); + }; + const cancelMulti = (): void => { + setValue(initialValue); + setIsEditing(false); + }; + return ( +