import type { Table } from "@tanstack/react-table"; import { createContext, useContext, useState, useMemo, type PropsWithChildren } from "react"; import type { Dispatch, SetStateAction } from "react"; import { DataTableSelectionBar, type ColumnFilter } from "@calcom/features/data-table"; import classNames from "@calcom/lib/classNames"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import slugify from "@calcom/lib/slugify"; import type { Attribute as _Attribute, AttributeOption } from "@calcom/prisma/client"; import { trpc } from "@calcom/trpc"; import { Alert, Button, Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, CommandList, Icon, Input, Popover, PopoverContent, PopoverTrigger, showToast, } from "@calcom/ui"; import type { UserTableUser } from "../types"; interface Props { table: Table; filters: ColumnFilter[]; } type Attribute = _Attribute & { options: AttributeOption[] }; type AttributesContextType = { selectedAttribute: string | undefined; setSelectedAttribute: Dispatch>; foundAttributeInCache: Attribute | undefined; selectedAttributeOptions: string[]; setSelectedAttributeOptions: Dispatch>; attributes: Attribute[] | undefined; }; const AttributesContext = createContext(null); function AttributesProvider({ children }: PropsWithChildren) { const { data: attributes } = trpc.viewer.attributes.list.useQuery(); const [selectedAttribute, setSelectedAttribute] = useState(); const [selectedAttributeOptions, setSelectedAttributeOptions] = useState([]); const foundAttributeInCache = useMemo( () => attributes?.find((attr) => attr.id === selectedAttribute), [selectedAttribute, attributes] ); const value: AttributesContextType = { selectedAttribute, setSelectedAttribute, selectedAttributeOptions, setSelectedAttributeOptions, foundAttributeInCache, attributes, }; return {children}; } function useAttributes() { const context = useContext(AttributesContext); if (!context) { throw new Error("useAttributes must be used within an AttributesProvider"); } return context; } function getTranslateableStringFromType(type: string) { switch (type) { case "SINGLE_SELECT": return "single_select"; case "MULTI_SELECT": return "multi_select"; case "TEXT": return "text"; case "NUMBER": return "number"; default: return undefined; } } function SelectedAttributeToAssign() { const { t } = useLocale(); const { foundAttributeInCache: foundAttribute, selectedAttributeOptions, setSelectedAttributeOptions, } = useAttributes(); if (!foundAttribute) { return null; } const translateableType = getTranslateableStringFromType(foundAttribute.type); const isSelectable = foundAttribute.type === "SINGLE_SELECT" || foundAttribute.type === "MULTI_SELECT"; return (
{foundAttribute.name} {translateableType && ({t(translateableType)})}
{isSelectable ? ( <> {foundAttribute.options.map((option) => { return ( { if (foundAttribute.type === "SINGLE_SELECT") { setSelectedAttributeOptions([option.id]); } else { setSelectedAttributeOptions((prev: string[]) => { if (prev.includes(option.id)) { return prev.filter((id: string) => id !== option.id); } return [...prev, option.id]; }); } }}> {option.value}
{selectedAttributeOptions?.includes(option.id) ? ( ) : null}
); })} ) : ( <> { // trigger onBlur so it's set as Apply is pressed (but not onChange) which triggers // a re-render which also loses focus. setSelectedAttributeOptions([e.target.value]); }} /> )}
); } function Content({ showMultiSelectWarning }: { showMultiSelectWarning: boolean }) { const { t } = useLocale(); const { selectedAttribute, setSelectedAttribute, selectedAttributeOptions, setSelectedAttributeOptions, attributes, foundAttributeInCache, } = useAttributes(); if (!selectedAttribute) { return ( <> No attributes found {attributes && attributes.map((option) => { return ( { setSelectedAttribute(option.id); setSelectedAttributeOptions([]); }}> {option.name}
); })}
); } if (showMultiSelectWarning) { return (
); } if (selectedAttribute) { return ; } return null; } function MassAssignAttributesBulkActionComponent({ table, filters }: Props) { const { selectedAttribute, setSelectedAttribute, selectedAttributeOptions, setSelectedAttributeOptions, foundAttributeInCache, } = useAttributes(); const [showMultiSelectWarning, setShowMultiSelectWarning] = useState(false); const { t } = useLocale(); const utils = trpc.useUtils(); const bulkAssignAttributes = trpc.viewer.attributes.bulkAssignAttributes.useMutation({ onSuccess: (success) => { // Optimistically update the infinite query data const selectedRows = table.getSelectedRowModel().flatRows; utils.viewer.organizations.listMembers.setInfiniteData( { limit: 10, searchTerm: "", expand: ["attributes"], filters, }, // @ts-expect-error i really dont know how to type this (oldData) => { if (!oldData) { return { pages: [], pageParams: [], }; } const newPages = oldData?.pages.map((page) => ({ ...page, rows: page.rows.map((row) => { if (selectedRows.some((selectedRow) => selectedRow.original.id === row.id)) { // Update the attributes for the selected users const attributeOptionValues = foundAttributeInCache?.options.filter((option) => selectedAttributeOptions.includes(option.id) ); const newAttributes = row.attributes?.filter((attr) => attr.attributeId !== selectedAttribute) || []; if (attributeOptionValues && attributeOptionValues.length > 0) { const newAttributeValues = attributeOptionValues?.map((value) => ({ id: value.id, attributeId: value.attributeId, value: value.value, slug: value.slug, contains: value.contains, isGroup: value.isGroup, weight: 100, })); newAttributes.push(...newAttributeValues); } else { // Text or number input we don't have an option to fall back on newAttributes.push({ id: "-1", attributeId: foundAttributeInCache?.id ?? "-1", value: selectedAttributeOptions[0], slug: slugify(selectedAttributeOptions[0]), contains: [], isGroup: false, weight: 100, }); } return { ...row, attributes: newAttributes, }; } return row; }), })); return { ...oldData, pages: newPages, }; } ); setSelectedAttribute(undefined); setSelectedAttributeOptions([]); showToast(success.message, "success"); }, onError: (error) => { showToast(`Error assigning attributes: ${error.message}`, "error"); }, }); return ( { if (!open) { setSelectedAttribute(undefined); setSelectedAttributeOptions([]); setShowMultiSelectWarning(false); } }}> {t("add_attributes")} {/* We dont really use shadows much - but its needed here */}
{selectedAttribute ? ( <> ) : null}
); } export function MassAssignAttributesBulkAction({ table, filters }: Props) { return ( ); }