import { t } from "@lingui/core/macro"; import { Trans } from "@lingui/react/macro"; import { useEffect } from "react"; import { useForm } from "react-hook-form"; import { HiOutlineBarsArrowDown, HiOutlineBarsArrowUp, HiXMark, } from "react-icons/hi2"; import type { NewCardInput } from "@kan/api/types"; import { generateUID } from "@kan/shared/utils"; import type { WorkspaceMember } from "~/components/Editor"; import Avatar from "~/components/Avatar"; import Button from "~/components/Button"; import CheckboxDropdown from "~/components/CheckboxDropdown"; import Editor from "~/components/Editor"; import Input from "~/components/Input"; import LabelIcon from "~/components/LabelIcon"; import Toggle from "~/components/Toggle"; import { useModalFormState } from "~/hooks/useModalFormState"; import { useModal } from "~/providers/modal"; import { usePopup } from "~/providers/popup"; import { api } from "~/utils/api"; import { formatMemberDisplayName, getAvatarUrl } from "~/utils/helpers"; type NewCardFormInput = NewCardInput & { isCreateAnotherEnabled: boolean; }; interface QueryParams { boardPublicId: string; members: string[]; labels: string[]; } interface NewCardFormProps { isTemplate: boolean; boardPublicId: string; listPublicId: string; queryParams: QueryParams; } export function NewCardForm({ isTemplate, boardPublicId, listPublicId, queryParams, }: NewCardFormProps) { const { showPopup } = usePopup(); const { closeModal, openModal, modalStates, clearModalState } = useModal(); const utils = api.useUtils(); // persists the form values const { formState, saveFormState } = useModalFormState({ modalType: "NEW_CARD", initialValues: { title: "", description: "", listPublicId, labelPublicIds: [], memberPublicIds: [], isCreateAnotherEnabled: false, position: "start", }, resetOnClose: true, }); const { register, handleSubmit, reset, setValue, watch } = useForm({ values: formState, }); const labelPublicIds = watch("labelPublicIds") || []; const memberPublicIds = watch("memberPublicIds") || []; const isCreateAnotherEnabled = watch("isCreateAnotherEnabled"); const position = watch("position"); const title = watch("title"); const description = watch("description"); // saving form state whenever form values change useEffect(() => { const subscription = watch((data) => { saveFormState(data as NewCardFormInput); }); return () => subscription.unsubscribe(); }, [watch, saveFormState]); const { data: boardData } = api.board.byId.useQuery(queryParams, { enabled: !!boardPublicId, }); // this adds the new created label to selected labels useEffect(() => { const newLabelId = modalStates.NEW_LABEL_CREATED; if (newLabelId !== undefined && !labelPublicIds.includes(newLabelId)) { setValue("labelPublicIds", [...labelPublicIds, newLabelId]); } }, [modalStates, labelPublicIds]); // this removes the deleted label from selected labels if it is selected useEffect(() => { if (boardData?.labels) { const availableLabelIds = boardData.labels.map((label) => label.publicId); const newLabelId = modalStates.NEW_LABEL_CREATED; if (newLabelId && availableLabelIds.includes(newLabelId)) { clearModalState("NEW_LABEL_CREATED"); } const validLabelIds = labelPublicIds.filter( (id) => availableLabelIds.includes(id) || id === newLabelId, ); if (validLabelIds.length !== labelPublicIds.length) { setValue("labelPublicIds", validLabelIds); } } }, [boardData?.labels, labelPublicIds, modalStates.NEW_LABEL_CREATED]); const createCard = api.card.create.useMutation({ onMutate: async (args) => { await utils.board.byId.cancel(); const currentState = utils.board.byId.getData(queryParams); utils.board.byId.setData(queryParams, (oldBoard) => { if (!oldBoard) return oldBoard; const updatedLists = oldBoard.lists.map((list) => { if (list.publicId === listPublicId) { const newCard = { publicId: `PLACEHOLDER_${generateUID()}`, title: args.title, listId: 2, description: "", labels: oldBoard.labels.filter((label) => args.labelPublicIds.includes(label.publicId), ), members: oldBoard.workspace.members .filter((member) => args.memberPublicIds.includes(member.publicId), ) .map((member) => ({ ...member, deletedAt: null, })) ?? [], _filteredLabels: labelPublicIds.map((id) => ({ publicId: id })), _filteredMembers: memberPublicIds.map((id) => ({ publicId: id })), index: position === "start" ? 0 : list.cards.length, }; const updatedCards = position === "start" ? [newCard, ...list.cards] : [...list.cards, newCard]; return { ...list, cards: updatedCards }; } return list; }); return { ...oldBoard, lists: updatedLists }; }); return { previousState: currentState }; }, onError: (error, _newList, context) => { utils.board.byId.setData(queryParams, context?.previousState); showPopup({ header: t`Unable to create card`, message: error.data?.zodError?.fieldErrors.title?.[0] ? `${error.data.zodError.fieldErrors.title[0].replace("String", "Title")}` : t`Please try again later, or contact customer support.`, icon: "error", }); }, onSuccess: async () => { const isCreateAnotherEnabled = watch("isCreateAnotherEnabled"); if (!isCreateAnotherEnabled) { // close modal (state will auto-clear due to resetOnClose: true) closeModal(); } else { // reset form for creating another card const newFormState = { title: "", description: "", listPublicId: watch("listPublicId"), labelPublicIds: [], memberPublicIds: [], isCreateAnotherEnabled, position, }; reset(newFormState); saveFormState(newFormState); } await utils.board.byId.invalidate(queryParams); }, }); useEffect(() => { const titleElement: HTMLElement | null = document.querySelector("#title"); if (titleElement) titleElement.focus(); }, []); const formattedLabels = boardData?.labels.map((label) => ({ key: label.publicId, value: label.name, leftIcon: , selected: labelPublicIds.includes(label.publicId), })) ?? []; const formattedLists = boardData?.lists.map((list) => ({ key: list.publicId, value: list.name, selected: list.publicId === watch("listPublicId"), })) ?? []; const formattedMembers = boardData?.workspace.members.map((member) => ({ key: member.publicId, value: formatMemberDisplayName( member.user?.name ?? null, member.user?.email ?? member.email, ), selected: memberPublicIds.includes(member.publicId), leftIcon: ( ), })) ?? []; const onSubmit = (data: NewCardInput) => { createCard.mutate({ title: data.title, description: data.description, listPublicId: data.listPublicId, labelPublicIds: data.labelPublicIds, memberPublicIds: data.memberPublicIds, position: data.position, }); }; const handleToggleCreateAnother = (): void => { setValue("isCreateAnotherEnabled", !isCreateAnotherEnabled); }; const handleSelectList = (listPublicId: string): void => { setValue("listPublicId", listPublicId); }; const handleSelectMembers = (memberPublicId: string): void => { const currentIndex = memberPublicIds.indexOf(memberPublicId); if (currentIndex === -1) { setValue("memberPublicIds", [...memberPublicIds, memberPublicId]); } else { const newMemberPublicIds = [...memberPublicIds]; newMemberPublicIds.splice(currentIndex, 1); setValue("memberPublicIds", newMemberPublicIds); } }; const handleSelectLabels = (labelPublicId: string): void => { const currentIndex = labelPublicIds.indexOf(labelPublicId); if (currentIndex === -1) { setValue("labelPublicIds", [...labelPublicIds, labelPublicId]); } else { const newLabelPublicIds = [...labelPublicIds]; newLabelPublicIds.splice(currentIndex, 1); setValue("labelPublicIds", newLabelPublicIds); } }; const selectedList = formattedLists.find((item) => item.selected); return (

{t`New card`}

{ if (e.key === "Enter") { e.preventDefault(); await handleSubmit(onSubmit)(); } }} />
{ setValue("description", value); saveFormState({ ...formState, description: value }); }} workspaceMembers={ boardData?.workspace.members?.map( (member): WorkspaceMember => ({ publicId: member.publicId, email: member.email, user: member.user ? { id: member.publicId, name: member.user.name, image: member.user.image ?? null, } : null, }), ) ?? [] } />
handleSelectList(item.key)} >
{selectedList?.value}
{!isTemplate && (
handleSelectMembers(item.key) } >
{!memberPublicIds.length ? ( t`Members` ) : (
{memberPublicIds.map((memberPublicId) => { const member = formattedMembers.find( (member) => member.key === memberPublicId, ); return ( {member?.value .split(" ") .map((namePart) => namePart.charAt(0).toUpperCase(), ) .join("")} ); })}
)}
)}
handleSelectLabels(item.key)} handleEdit={(labelPublicId) => openModal("EDIT_LABEL", labelPublicId) } handleCreate={() => openModal("NEW_LABEL")} createNewItemLabel={t`Create new label`} >
{!labelPublicIds.length ? ( t`Labels` ) : ( <>
1 ? "flex -space-x-[2px] overflow-hidden" : "flex items-center" } > {labelPublicIds.map((labelPublicId) => { const label = boardData?.labels.find( (label) => label.publicId === labelPublicId, ); return ( <> {labelPublicIds.length === 1 && (
{label?.name}
)} ); })}
{labelPublicIds.length > 1 && (
{`${labelPublicIds.length} labels`}
)} )}
); }