* fix * move event types components to web * update import paths * mv apps components * migrate form builder * fix * mv sso * fix * mv * update import paths * update import paths * mv * mv * mv * fix * update Booker * fix * fix * fix * fix * mv video * mv embed components to web * update import paths * mv calendar weekly view components * update import paths * fix * fixp * fix * fix * fix * fix: update FormBuilder imports to use @calcom/features/form-builder Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update broken import paths after file migrations Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: correct import paths for platform atoms and moved components Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: apply CSS type fixes and add missing atoms exports Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: resolve type errors in test files after component migrations Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: resolve remaining type errors in test files Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix * migrate * fix: resolve type errors in test and mock files - Add missing bookingForm, bookerFormErrorRef, instantConnectCooldownMs to Booker.test.tsx bookings prop - Add all required BookerEvent properties to event.mock.ts - Add vi import from vitest to all mock files - Fix date parameter types in packages/dayjs/__mocks__/index.ts - Add verificationCode and setVerificationCode to test-utils.tsx mock store - Remove children.type access in Section.tsx mock to fix type error - Fix lint issues: remove unused React imports, use import type where needed, add return types - Add biome-ignore comments for pre-existing lint warnings in test files Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * migrate * migrate * migrate * update import paths * update import paths * update import paths * fix * migrate data table * migrate data table * fix * fix * fix * migrate insights components * migrate insights components * fix * mv * update import paths * fix * fix * fix * fix * fix * fix: resolve type errors in test mocks - Booker.test.tsx: Add all required UseFormReturn methods to bookingForm mock - event.mock.ts: Fix entity, subsetOfHosts, instantMeetingParameters, fieldTranslations, image types - dayjs/__mocks__/index.ts: Use Object.assign for proper typing of mock properties - Section.tsx: Change 'class' to 'className' in JSX with biome-ignore comment Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing hasDataErrors and dataErrors to bookings.errors mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing loadingStates properties to bookings mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing slots properties (setTentativeSelectedTimeslots, tentativeSelectedTimeslots, slotReservationId) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: update quickAvailabilityChecks to include utcEndIso and use valid status type Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing bookerForm properties (formName, beforeVerifyEmail, formErrors) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing UseFormReturn properties to bookerForm.bookingForm mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add hasFormErrors and formErrors to bookerForm.formErrors mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add hasFormErrors and formErrors to bookerForm.errors mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing isError property to mockEvent Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: use complete BookerEvent mock in Booker.test.tsx Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: use branded bookingFields type in event mock Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: add missing schedule mock properties (isError, isSuccess, isLoading, dataUpdatedAt) Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * revert * fix * fix * fix * fix build * fix * fix * fix * fix: correct AddMembersWithSwitch test wrapper to use initial assignAllTeamMembers value - Fixed test wrapper to initialize useState with componentProps.assignAllTeamMembers instead of hardcoded false, allowing tests to properly test different states - Updated test expectations for ALL_TEAM_MEMBERS_ENABLED_AND_SEGMENT_NOT_APPLICABLE state to match actual component behavior (toggle should be present and checked) - Fixed 'should show Segment when toggled on' test to start with assignAllTeamMembers: false to properly test the flow of enabling it - Added explicit types to satisfy biome lint requirements Co-Authored-By: benny@cal.com <sldisek783@gmail.com> * fix: use JSX.Element instead of React.JSX.Element for type compatibility Co-Authored-By: benny@cal.com <sldisek783@gmail.com> --------- Co-authored-by: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com>
343 lines
11 KiB
TypeScript
343 lines
11 KiB
TypeScript
import type { Table } from "@tanstack/react-table";
|
|
import { createContext, useContext, useState, useMemo, type PropsWithChildren } from "react";
|
|
import type { Dispatch, SetStateAction } from "react";
|
|
|
|
import { type ColumnFilter } from "@calcom/features/data-table";
|
|
import { DataTableSelectionBar } from "~/data-table/components";
|
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
import type { Attribute as _Attribute, AttributeOption } from "@calcom/prisma/client";
|
|
import { trpc } from "@calcom/trpc/react";
|
|
import classNames from "@calcom/ui/classNames";
|
|
import { Alert } from "@calcom/ui/components/alert";
|
|
import { Button } from "@calcom/ui/components/button";
|
|
import {
|
|
Command,
|
|
CommandEmpty,
|
|
CommandGroup,
|
|
CommandInput,
|
|
CommandItem,
|
|
CommandList,
|
|
} from "@calcom/ui/components/command";
|
|
import { Input } from "@calcom/ui/components/form";
|
|
import { Icon } from "@calcom/ui/components/icon";
|
|
import { Popover, PopoverContent, PopoverTrigger } from "@calcom/ui/components/popover";
|
|
import { showToast } from "@calcom/ui/components/toast";
|
|
|
|
import type { UserTableUser } from "../types";
|
|
|
|
interface Props {
|
|
table: Table<UserTableUser>;
|
|
filters: ColumnFilter[];
|
|
}
|
|
|
|
type Attribute = _Attribute & { options: AttributeOption[] };
|
|
|
|
type AttributesContextType = {
|
|
selectedAttribute: string | undefined;
|
|
setSelectedAttribute: Dispatch<SetStateAction<string | undefined>>;
|
|
foundAttributeInCache: Attribute | undefined;
|
|
|
|
selectedAttributeOptions: string[];
|
|
setSelectedAttributeOptions: Dispatch<SetStateAction<string[]>>;
|
|
|
|
attributes: Attribute[] | undefined;
|
|
};
|
|
|
|
const AttributesContext = createContext<AttributesContextType | null>(null);
|
|
|
|
function AttributesProvider({ children }: PropsWithChildren) {
|
|
const { data: attributes } = trpc.viewer.attributes.list.useQuery();
|
|
const [selectedAttribute, setSelectedAttribute] = useState<string>();
|
|
const [selectedAttributeOptions, setSelectedAttributeOptions] = useState<string[]>([]);
|
|
|
|
const foundAttributeInCache = useMemo(
|
|
() => attributes?.find((attr) => attr.id === selectedAttribute),
|
|
[selectedAttribute, attributes]
|
|
);
|
|
|
|
const value: AttributesContextType = {
|
|
selectedAttribute,
|
|
setSelectedAttribute,
|
|
selectedAttributeOptions,
|
|
setSelectedAttributeOptions,
|
|
foundAttributeInCache,
|
|
attributes,
|
|
};
|
|
|
|
return <AttributesContext.Provider value={value}>{children}</AttributesContext.Provider>;
|
|
}
|
|
|
|
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 (
|
|
<CommandList>
|
|
<div className="flex items-center gap-2 border-b px-3 py-2">
|
|
<span className="block">{foundAttribute.name}</span>
|
|
{translateableType && <span className="text-muted block text-xs">({t(translateableType)})</span>}
|
|
</div>
|
|
<CommandGroup>
|
|
{isSelectable ? (
|
|
<>
|
|
{foundAttribute.options.map((option) => {
|
|
return (
|
|
<CommandItem
|
|
key={option.id}
|
|
className="hover:cursor-pointer"
|
|
onSelect={() => {
|
|
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];
|
|
});
|
|
}
|
|
}}>
|
|
<span>{option.value}</span>
|
|
<div
|
|
className={classNames(
|
|
"ml-auto flex h-4 w-4 items-center justify-center rounded-sm border"
|
|
)}>
|
|
{selectedAttributeOptions?.includes(option.id) ? (
|
|
<Icon name="check" className={classNames("h-4 w-4")} />
|
|
) : null}
|
|
</div>
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</>
|
|
) : (
|
|
<>
|
|
<CommandItem>
|
|
<Input
|
|
defaultValue={selectedAttributeOptions[0] || ""}
|
|
type={foundAttribute.type === "TEXT" ? "text" : "number"}
|
|
onBlur={(e) => {
|
|
// 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]);
|
|
}}
|
|
/>
|
|
</CommandItem>
|
|
</>
|
|
)}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
);
|
|
}
|
|
|
|
function Content({ showMultiSelectWarning }: { showMultiSelectWarning: boolean }) {
|
|
const { t } = useLocale();
|
|
const {
|
|
selectedAttribute,
|
|
setSelectedAttribute,
|
|
selectedAttributeOptions,
|
|
setSelectedAttributeOptions,
|
|
attributes,
|
|
foundAttributeInCache,
|
|
} = useAttributes();
|
|
if (!selectedAttribute) {
|
|
return (
|
|
<>
|
|
<CommandInput placeholder={t("search")} />
|
|
<CommandList>
|
|
<CommandEmpty>No attributes found</CommandEmpty>
|
|
<CommandGroup>
|
|
{attributes &&
|
|
attributes.map((option) => {
|
|
return (
|
|
<CommandItem
|
|
key={option.id}
|
|
className="hover:cursor-pointer"
|
|
onSelect={() => {
|
|
setSelectedAttribute(option.id);
|
|
setSelectedAttributeOptions([]);
|
|
}}>
|
|
<span>{option.name}</span>
|
|
<div
|
|
className={classNames("ml-auto flex h-4 w-4 items-center justify-center rounded-sm")}>
|
|
<Icon name="chevron-right" className={classNames("h-4 w-4")} />
|
|
</div>
|
|
</CommandItem>
|
|
);
|
|
})}
|
|
</CommandGroup>
|
|
</CommandList>
|
|
</>
|
|
);
|
|
}
|
|
|
|
if (showMultiSelectWarning) {
|
|
return (
|
|
<div className="max-h-[300px] overflow-y-auto overflow-x-hidden px-3 py-2">
|
|
<Alert
|
|
severity="warning"
|
|
message="You are mass assigning to a multi select. This will assign the attribute to all selected users and
|
|
will not override existing values."
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (selectedAttribute) {
|
|
return <SelectedAttributeToAssign />;
|
|
}
|
|
|
|
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) => {
|
|
setSelectedAttribute(undefined);
|
|
setSelectedAttributeOptions([]);
|
|
utils.viewer.organizations.listMembers.invalidate();
|
|
showToast(success.message, "success");
|
|
},
|
|
onError: (error) => {
|
|
showToast(`Error assigning attributes: ${error.message}`, "error");
|
|
},
|
|
});
|
|
|
|
return (
|
|
<Popover
|
|
onOpenChange={(open) => {
|
|
if (!open) {
|
|
setSelectedAttribute(undefined);
|
|
setSelectedAttributeOptions([]);
|
|
setShowMultiSelectWarning(false);
|
|
}
|
|
}}>
|
|
<PopoverTrigger asChild>
|
|
<DataTableSelectionBar.Button icon="tags" color="secondary">
|
|
{t("add_attributes")}
|
|
</DataTableSelectionBar.Button>
|
|
</PopoverTrigger>
|
|
{/* We dont really use shadows much - but its needed here */}
|
|
<PopoverContent className="p-0 shadow-md" align="start" sideOffset={12}>
|
|
<Command>
|
|
<Content showMultiSelectWarning={showMultiSelectWarning} />
|
|
</Command>
|
|
<div className="my-1.5 flex w-full justify-end gap-2 p-1.5">
|
|
{selectedAttribute ? (
|
|
<>
|
|
<Button
|
|
color="secondary"
|
|
className="rounded-md"
|
|
size="sm"
|
|
onClick={() => {
|
|
setSelectedAttribute(undefined);
|
|
setSelectedAttributeOptions([]);
|
|
setShowMultiSelectWarning(false);
|
|
}}>
|
|
{t("clear")}
|
|
</Button>
|
|
<Button
|
|
className="rounded-md"
|
|
loading={bulkAssignAttributes.isPending}
|
|
size="sm"
|
|
onClick={() => {
|
|
if (
|
|
foundAttributeInCache &&
|
|
foundAttributeInCache.type === "MULTI_SELECT" &&
|
|
!showMultiSelectWarning
|
|
) {
|
|
setShowMultiSelectWarning(true);
|
|
} else {
|
|
if (!foundAttributeInCache) {
|
|
return;
|
|
}
|
|
setShowMultiSelectWarning(false);
|
|
let attributesToAssign;
|
|
if (
|
|
foundAttributeInCache?.type === "MULTI_SELECT" ||
|
|
foundAttributeInCache?.type === "SINGLE_SELECT"
|
|
) {
|
|
attributesToAssign = [
|
|
{
|
|
id: foundAttributeInCache.id,
|
|
options: selectedAttributeOptions.map((v) => ({
|
|
value: v,
|
|
})),
|
|
},
|
|
];
|
|
} else {
|
|
attributesToAssign = [
|
|
{ id: foundAttributeInCache.id, value: selectedAttributeOptions[0] },
|
|
];
|
|
}
|
|
|
|
bulkAssignAttributes.mutate({
|
|
attributes: attributesToAssign,
|
|
userIds: table.getSelectedRowModel().rows.map((row) => row.original.id),
|
|
});
|
|
}
|
|
}}>
|
|
{t("apply")}
|
|
</Button>
|
|
</>
|
|
) : null}
|
|
</div>
|
|
</PopoverContent>
|
|
</Popover>
|
|
);
|
|
}
|
|
|
|
export function MassAssignAttributesBulkAction({ table, filters }: Props) {
|
|
return (
|
|
<AttributesProvider>
|
|
<MassAssignAttributesBulkActionComponent table={table} filters={filters} />
|
|
</AttributesProvider>
|
|
);
|
|
}
|