Enabling workflows for managed events (#8446)

* Enabling workflows for managed events

* Disabling fields for workflow when readonly

* Disabling event type workflows if readonly

* Installing auth app

* Reverting yarn.lock

* Again reverting yarn

* Showing user workflows in managed event type

* Reusing existing code to show all workflows

* Further extending workflow support

* Added unit test

* Fixing workflow assignment

* Fixing locked workflows for children MET

* Update packages/features/ee/workflows/components/EventWorkflowsTab.tsx

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>

* Personal workflows fix, matching count numbers

* Fixing menu items for managed event types

* Fixing type checks

* Fixing empty activeOn select

* Fixing unit test

* handling active workflows for MET

* Fixing MET slug nit

* Embed option in action restored for non-MET

* Correcting mobile view when hidden

* More adjustments

* fix sms reminder field for children

* remove console.log

---------

Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
This commit is contained in:
Leo Giovanetti 2023-05-15 10:56:26 -03:00 committed by GitHub
parent 372cd94d9f
commit cb2225259c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
29 changed files with 559 additions and 269 deletions

View File

@ -390,7 +390,12 @@ export const EventSetupTab = (
addOnLeading={
<>
{CAL_URL?.replace(/^(https?:|)\/\//, "")}/
{team ? "team/" + team.slug : eventType.users[0].username}/
{!isManagedEventType
? team
? "team/" + team.slug
: eventType.users[0].username
: t("username_placeholder")}
/
</>
}
{...formMethods.register("slug", {

View File

@ -170,7 +170,7 @@ function EventTypeSingleLayout({
// Define tab navigation here
const EventTypeTabs = useMemo(() => {
let navigation = getNavigation({
const navigation = getNavigation({
t,
eventType,
enabledAppsNumber,
@ -210,7 +210,7 @@ function EventTypeSingleLayout({
}
if (isManagedEventType || isChildrenManagedEventType) {
// Removing apps and workflows for manageg event types by admins v1
navigation = navigation.slice(0, -2);
navigation.splice(-2, 1);
} else {
navigation.push({
name: "webhooks",

View File

@ -157,10 +157,13 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
};
}
let deploymentKey = await getDeploymentKey(prisma);
const deploymentKey = await prisma.deployment.findUnique({
where: { id: 1 },
select: { licenseKey: true },
});
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey) {
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey?.licenseKey) {
await prisma.deployment.upsert({
where: { id: 1 },
update: {
@ -172,10 +175,9 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
agreedLicenseAt: new Date(),
},
});
deploymentKey = await getDeploymentKey(prisma);
}
const isFreeLicense = deploymentKey === "";
const isFreeLicense = (await getDeploymentKey(prisma)) === "";
return {
props: {

View File

@ -490,18 +490,20 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
{t("duplicate")}
</DropdownItem>
</DropdownMenuItem>
<DropdownMenuItem className="outline-none">
<EmbedButton
as={DropdownItem}
type="button"
StartIcon={Code}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
{t("embed")}
</EmbedButton>
</DropdownMenuItem>
</>
)}
{!isManagedEventType && (
<DropdownMenuItem className="outline-none">
<EmbedButton
as={DropdownItem}
type="button"
StartIcon={Code}
className="w-full rounded-none"
embedUrl={encodeURIComponent(embedLink)}>
{t("embed")}
</EmbedButton>
</DropdownMenuItem>
)}
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
!isChildrenManagedEventType && (

View File

@ -1195,6 +1195,7 @@
"create_workflow": "Create a workflow",
"do_this": "Do this",
"turn_off": "Turn off",
"turn_on": "Turn on",
"settings_updated_successfully": "Settings updated successfully",
"error_updating_settings": "Error updating settings",
"personal_cal_url": "My personal {{appName}} URL",
@ -1740,7 +1741,7 @@
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
"locked_by_admin": "Locked by admin",
"locked_by_admin": "Locked by team admin",
"app_not_connected": "You have not connected a {{appName}} account.",
"connect_now": "Connect now",
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",

View File

@ -3,7 +3,7 @@ import type { Prisma } from "@prisma/client";
import updateChildrenEventTypes from "@calcom/features/ee/managed-event-types/lib/handleChildrenEventTypes";
import { buildEventType } from "@calcom/lib/test/builder";
import type { CompleteEventType } from "@calcom/prisma/zod";
import type { CompleteEventType, CompleteWorkflowsOnEventTypes } from "@calcom/prisma/zod";
import { prismaMock } from "../../../../tests/config/singleton";
@ -291,4 +291,85 @@ describe("handleChildrenEventTypes", () => {
expect(result.deletedExistentEventTypes).toEqual([123]);
});
});
describe("Workflows", () => {
it("Links workflows to new and existing assigned members", async () => {
const { schedulingType, id, teamId, locations, timeZone, parentId, userId, ...evType } =
mockFindFirstEventType({
metadata: { managedEventConfig: {} },
locations: [],
workflows: [
{
workflowId: 11,
} as CompleteWorkflowsOnEventTypes,
],
});
prismaMock.$transaction.mockResolvedValue([{ id: 2 }]);
await updateChildrenEventTypes({
eventTypeId: 1,
oldEventType: { children: [{ userId: 4 }], team: { name: "" } },
children: [
{ hidden: false, owner: { id: 4, name: "", email: "", eventTypeSlugs: [] } },
{ hidden: false, owner: { id: 5, name: "", email: "", eventTypeSlugs: [] } },
],
updatedEventType: { schedulingType: "MANAGED", slug: "something" },
currentUserId: 1,
hashedLink: undefined,
connectedLink: null,
prisma: prismaMock,
});
expect(prismaMock.eventType.create).toHaveBeenCalledWith({
data: {
...evType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
hashedLink: undefined,
locations: [],
parentId: 1,
userId: 5,
users: {
connect: [
{
id: 5,
},
],
},
workflows: {
create: [{ workflowId: 11 }],
},
},
});
expect(prismaMock.eventType.update).toHaveBeenCalledWith({
data: {
...evType,
bookingLimits: undefined,
durationLimits: undefined,
recurringEvent: undefined,
hashedLink: undefined,
workflows: undefined,
scheduleId: undefined,
},
where: {
userId_parentId: {
userId: 4,
parentId: 1,
},
},
});
expect(prismaMock.workflowsOnEventTypes.upsert).toHaveBeenCalledWith({
create: {
eventTypeId: 2,
workflowId: 11,
},
update: {},
where: {
workflowId_eventTypeId: {
eventTypeId: 2,
workflowId: 11,
},
},
});
});
});
});

View File

@ -72,7 +72,7 @@ const CalendarSwitch = (props: ICalendarSwitchProps) => {
}
);
return (
<div className={classNames("flex flex-row items-center my-2")}>
<div className={classNames("my-2 flex flex-row items-center")}>
<div className="flex pl-2">
<Switch
id={externalId}

View File

@ -25,6 +25,7 @@ interface handleChildrenEventTypesProps {
oldEventType: {
children?: { userId: number | null }[] | null | undefined;
team: { name: string } | null;
workflows?: { workflowId: number }[];
} | null;
hashedLink: string | undefined;
connectedLink: { id: number } | null;
@ -145,6 +146,9 @@ export default async function handleChildrenEventTypes({
const newUserIds = currentUserIds?.filter((id) => !previousUserIds?.includes(id));
const oldUserIds = currentUserIds?.filter((id) => previousUserIds?.includes(id));
// Calculate if there are new workflows for which assigned members will get too
const currentWorkflowIds = eventType.workflows?.map((wf) => wf.workflowId);
// Define hashedLink query input
const hashedLinkQuery = (userId: number) => {
return hashedLink
@ -190,13 +194,11 @@ export default async function handleChildrenEventTypes({
},
parentId,
hidden: children?.find((ch) => ch.owner.id === userId)?.hidden ?? false,
// Reserved for v2
/*
workflows: eventType.workflows && {
createMany: {
data: eventType.workflows?.map((wf) => ({ ...wf, eventTypeId: undefined })),
},
workflows: currentWorkflowIds && {
create: currentWorkflowIds.map((wfId) => ({ workflowId: wfId })),
},
// Reserved for future releases
/*
webhooks: eventType.webhooks && {
createMany: {
data: eventType.webhooks?.map((wh) => ({ ...wh, eventTypeId: undefined })),
@ -221,7 +223,7 @@ export default async function handleChildrenEventTypes({
});
// Update event types for old users
await prisma.$transaction(
const oldEventTypes = await prisma.$transaction(
oldUserIds.map((userId) => {
return prisma.eventType.update({
where: {
@ -246,21 +248,30 @@ export default async function handleChildrenEventTypes({
})
);
// Reserved for v2
/*const updatedOldWorkflows = await prisma.workflow.updateMany({
where: {
userId: {
in: oldUserIds,
},
},
data: {
...eventType.workflows,
},
});
console.log(
"handleChildrenEventTypes:updatedOldWorkflows",
JSON.stringify({ updatedOldWorkflows }, null, 2)
);
if (currentWorkflowIds?.length) {
await prisma.$transaction(
currentWorkflowIds.flatMap((wfId) => {
return oldEventTypes.map((oEvTy) => {
return prisma.workflowsOnEventTypes.upsert({
create: {
eventTypeId: oEvTy.id,
workflowId: wfId,
},
update: {},
where: {
workflowId_eventTypeId: {
eventTypeId: oEvTy.id,
workflowId: wfId,
},
},
});
});
})
);
}
// Reserved for future releases
/**
const updatedOldWebhooks = await prisma.webhook.updateMany({
where: {
userId: {

View File

@ -23,6 +23,7 @@ type ItemProps = {
id: number;
title: string;
};
isChildrenManagedEventType: boolean;
};
const WorkflowListItem = (props: ItemProps) => {
@ -131,18 +132,33 @@ const WorkflowListItem = (props: ItemProps) => {
})}
</div>
</div>
<div className="flex-none">
<Link href={`/workflows/${workflow.id}`} passHref={true} target="_blank">
<Button type="button" color="minimal" className="mr-4">
<div className="hidden ltr:mr-2 rtl:ml-2 sm:block">{t("edit")}</div>
<ExternalLink className="text-default -mt-[2px] h-4 w-4 stroke-2" />
</Button>
</Link>
</div>
<Tooltip content={t("turn_off") as string}>
<div className="ltr:mr-2 rtl:ml-2">
{!workflow.readOnly && (
<div className="flex-none">
<Link href={`/workflows/${workflow.id}`} passHref={true} target="_blank">
<Button type="button" color="minimal" className="mr-4">
<div className="hidden ltr:mr-2 rtl:ml-2 sm:block">{t("edit")}</div>
<ExternalLink className="text-default -mt-[2px] h-4 w-4 stroke-2" />
</Button>
</Link>
</div>
)}
<Tooltip
content={
t(
workflow.readOnly && props.isChildrenManagedEventType
? "locked_by_admin"
: isActive
? "turn_off"
: "turn_on"
) as string
}>
<div className="flex items-center ltr:mr-2 rtl:ml-2">
{workflow.readOnly && props.isChildrenManagedEventType && (
<Lock className="text-subtle h-4 w-4 ltr:mr-2 rtl:ml-2" />
)}
<Switch
checked={isActive}
disabled={workflow.readOnly}
onCheckedChange={() => {
activateEventTypeMutation.mutate({ workflowId: workflow.id, eventTypeId: eventType.id });
}}
@ -163,9 +179,14 @@ type Props = {
function EventWorkflowsTab(props: Props) {
const { workflows, eventType } = props;
const { t } = useLocale();
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
const { data, isLoading } = trpc.viewer.workflows.list.useQuery({
teamId: eventType.team?.id,
userId: eventType.userId || undefined,
userId: !isChildrenManagedEventType ? eventType.userId || undefined : undefined,
});
const router = useRouter();
const [sortedWorkflows, setSortedWorkflows] = useState<Array<WorkflowType>>([]);
@ -173,7 +194,11 @@ function EventWorkflowsTab(props: Props) {
useEffect(() => {
if (data?.workflows) {
const activeWorkflows = workflows.map((workflowOnEventType) => {
return workflowOnEventType;
const dataWf = data.workflows.find((wf) => wf.id === workflowOnEventType.id);
return {
...workflowOnEventType,
readOnly: isChildrenManagedEventType && dataWf?.teamId ? true : dataWf?.readOnly ?? true,
} as WorkflowType;
});
const disabledWorkflows = data.workflows.filter(
(workflow) =>
@ -204,12 +229,6 @@ function EventWorkflowsTab(props: Props) {
},
});
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
return (
<LicenseRequired>
{!isLoading ? (
@ -217,6 +236,7 @@ function EventWorkflowsTab(props: Props) {
{isManagedEventType && (
<Alert
severity="neutral"
className="mb-2"
title={t("locked_for_members")}
message={t("locked_workflows_description")}
/>
@ -226,7 +246,12 @@ function EventWorkflowsTab(props: Props) {
<div className="space-y-4">
{sortedWorkflows.map((workflow) => {
return (
<WorkflowListItem key={workflow.id} workflow={workflow} eventType={props.eventType} />
<WorkflowListItem
key={workflow.id}
workflow={workflow}
eventType={props.eventType}
isChildrenManagedEventType
/>
);
})}
</div>
@ -238,19 +263,13 @@ function EventWorkflowsTab(props: Props) {
headline={t("workflows")}
description={t("no_workflows_description")}
buttonRaw={
isChildrenManagedEventType && !isManagedEventType ? (
<Button StartIcon={Lock} color="secondary" disabled>
{t("locked_by_admin")}
</Button>
) : (
<Button
target="_blank"
color="secondary"
onClick={() => createMutation.mutate({ teamId: eventType.team?.id })}
loading={createMutation.isLoading}>
{t("create_workflow")}
</Button>
)
<Button
target="_blank"
color="secondary"
onClick={() => createMutation.mutate({ teamId: eventType.team?.id })}
loading={createMutation.isLoading}>
{t("create_workflow")}
</Button>
}
/>
</div>

View File

@ -17,6 +17,7 @@ import type { FormValues } from "../pages/workflow";
type Props = {
form: UseFormReturn<FormValues>;
disabled: boolean;
};
export const TimeTimeUnitInput = (props: Props) => {
@ -33,6 +34,7 @@ export const TimeTimeUnitInput = (props: Props) => {
type="number"
min="1"
label=""
disabled={props.disabled}
defaultValue={form.getValues("time") || 24}
className="-mt-2 rounded-r-none text-sm focus:ring-0"
{...form.register("time", { valueAsNumber: true })}

View File

@ -6,7 +6,7 @@ import { Controller } from "react-hook-form";
import { SENDER_ID, SENDER_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WorkflowTemplates, SchedulingType } from "@calcom/prisma/enums";
import { WorkflowTemplates } from "@calcom/prisma/enums";
import type { WorkflowActions } from "@calcom/prisma/enums";
import { trpc } from "@calcom/trpc/react";
import type { MultiSelectCheckboxesOptionType as Option } from "@calcom/ui";
@ -26,6 +26,7 @@ interface Props {
setSelectedEventTypes: Dispatch<SetStateAction<Option[]>>;
teamId?: number;
isMixedEventType: boolean;
readOnly: boolean;
}
export default function WorkflowDetailsPage(props: Props) {
@ -48,15 +49,10 @@ export default function WorkflowDetailsPage(props: Props) {
if (teamId && teamId !== group.teamId) return options;
return [
...options,
...group.eventTypes
.filter(
(evType) =>
!evType.metadata?.managedEventConfig && evType.schedulingType !== SchedulingType.MANAGED
)
.map((eventType) => ({
value: String(eventType.id),
label: eventType.title,
})),
...group.eventTypes.map((eventType) => ({
value: String(eventType.id),
label: `${eventType.title} ${eventType.children.length ? `(+${eventType.children.length})` : ``}`,
})),
];
}, [] as Option[]) || [],
[data]
@ -117,7 +113,12 @@ export default function WorkflowDetailsPage(props: Props) {
<div className="my-8 sm:my-0 md:flex">
<div className="pl-2 pr-3 md:sticky md:top-6 md:h-0 md:pl-0">
<div className="mb-5">
<TextField label={`${t("workflow_name")}:`} type="text" {...form.register("name")} />
<TextField
disabled={props.readOnly}
label={`${t("workflow_name")}:`}
type="text"
{...form.register("name")}
/>
</div>
<Label>{t("which_event_type_apply")}</Label>
<Controller
@ -127,6 +128,7 @@ export default function WorkflowDetailsPage(props: Props) {
return (
<MultiSelectCheckboxes
options={allEventTypeOptions}
isDisabled={props.readOnly}
isLoading={isLoading}
className="w-full md:w-64"
setSelected={setSelectedEventTypes}
@ -139,14 +141,16 @@ export default function WorkflowDetailsPage(props: Props) {
}}
/>
<div className="md:border-subtle my-7 border-transparent md:border-t" />
<Button
type="button"
StartIcon={Trash2}
color="destructive"
className="border"
onClick={() => setDeleteDialogOpen(true)}>
{t("delete_workflow")}
</Button>
{!props.readOnly && (
<Button
type="button"
StartIcon={Trash2}
color="destructive"
className="border"
onClick={() => setDeleteDialogOpen(true)}>
{t("delete_workflow")}
</Button>
)}
<div className="border-subtle my-7 border-t md:border-none" />
</div>
@ -154,7 +158,7 @@ export default function WorkflowDetailsPage(props: Props) {
<div className="bg-muted border-subtle w-full rounded-md border p-3 py-5 md:ml-3 md:p-8">
{form.getValues("trigger") && (
<div>
<WorkflowStepContainer form={form} teamId={teamId} />
<WorkflowStepContainer form={form} teamId={teamId} readOnly={props.readOnly} />
</div>
)}
{form.getValues("steps") && (
@ -168,23 +172,28 @@ export default function WorkflowDetailsPage(props: Props) {
reload={reload}
setReload={setReload}
teamId={teamId}
readOnly={props.readOnly}
/>
);
})}
</>
)}
<div className="my-3 flex justify-center">
<ArrowDown className="text-subtle stroke-[1.5px] text-3xl" />
</div>
<div className="flex justify-center">
<Button
type="button"
onClick={() => setIsAddActionDialogOpen(true)}
color="secondary"
className="bg-default">
{t("add_action")}
</Button>
</div>
{!props.readOnly && (
<>
<div className="my-3 flex justify-center">
<ArrowDown className="text-subtle stroke-[1.5px] text-3xl" />
</div>
<div className="flex justify-center">
<Button
type="button"
onClick={() => setIsAddActionDialogOpen(true)}
color="secondary"
className="bg-default">
{t("add_action")}
</Button>
</div>
</>
)}
</div>
</div>
<AddActionDialog

View File

@ -35,6 +35,10 @@ export type WorkflowType = Workflow & {
eventType: {
id: number;
title: string;
parentId: number | null;
_count: {
children: number;
};
};
}[];
readOnly?: boolean;
@ -112,12 +116,23 @@ export default function WorkflowListPage({ workflows, profileOptions, hasNoWorkf
<Badge variant="gray">
{workflow.activeOn && workflow.activeOn.length > 0 ? (
<Tooltip
content={workflow.activeOn.map((activeOn, key) => (
<p key={key}>{activeOn.eventType.title}</p>
))}>
content={workflow.activeOn
.filter((wf) => (workflow.teamId ? wf.eventType.parentId === null : true))
.map((activeOn, key) => (
<p key={key}>
{activeOn.eventType.title}
{activeOn.eventType._count.children > 0
? ` (+${activeOn.eventType._count.children})`
: ""}
</p>
))}>
<div>
<LinkIcon className="mr-1.5 inline h-3 w-3" aria-hidden="true" />
{t("active_on_event_types", { count: workflow.activeOn.length })}
{t("active_on_event_types", {
count: workflow.activeOn.filter((wf) =>
workflow.teamId ? wf.eventType.parentId === null : true
).length,
})}
</div>
</Tooltip>
) : (

View File

@ -52,6 +52,7 @@ type WorkflowStepProps = {
reload?: boolean;
setReload?: Dispatch<SetStateAction<boolean>>;
teamId?: number;
readOnly: boolean;
};
export default function WorkflowStepContainer(props: WorkflowStepProps) {
@ -249,6 +250,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Select
isSearchable={false}
className="text-sm"
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
form.setValue("trigger", val.value);
@ -281,11 +283,13 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{showTimeSection && (
<div className="mt-5">
<Label>{showTimeSectionAfter ? t("how_long_after") : t("how_long_before")}</Label>
<TimeTimeUnitInput form={form} />
<div className="mt-1 flex text-gray-500">
<Info className="mr-1 mt-0.5 h-4 w-4" />
<p className="text-sm">{t("testing_workflow_info_message")}</p>
</div>
<TimeTimeUnitInput form={form} disabled={props.readOnly} />
{!props.readOnly && (
<div className="mt-1 flex text-gray-500">
<Info className="mr-1 mt-0.5 h-4 w-4" />
<p className="text-sm">{t("testing_workflow_info_message")}</p>
</div>
)}
</div>
)}
</div>
@ -325,39 +329,41 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
</div>
</div>
</div>
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button type="button" color="minimal" variant="icon" StartIcon={MoreHorizontal} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={Trash2}
color="destructive"
onClick={() => {
const steps = form.getValues("steps");
const updatedSteps = steps
?.filter((currStep) => currStep.id !== step.id)
.map((s) => {
const updatedStep = s;
if (step.stepNumber < updatedStep.stepNumber) {
updatedStep.stepNumber = updatedStep.stepNumber - 1;
}
return updatedStep;
});
form.setValue("steps", updatedSteps);
if (setReload) {
setReload(!reload);
}
}}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
{!props.readOnly && (
<div>
<Dropdown>
<DropdownMenuTrigger asChild>
<Button type="button" color="minimal" variant="icon" StartIcon={MoreHorizontal} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<DropdownItem
type="button"
StartIcon={Trash2}
color="destructive"
onClick={() => {
const steps = form.getValues("steps");
const updatedSteps = steps
?.filter((currStep) => currStep.id !== step.id)
.map((s) => {
const updatedStep = s;
if (step.stepNumber < updatedStep.stepNumber) {
updatedStep.stepNumber = updatedStep.stepNumber - 1;
}
return updatedStep;
});
form.setValue("steps", updatedSteps);
if (setReload) {
setReload(!reload);
}
}}>
{t("delete")}
</DropdownItem>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
)}
</div>
<div className="border-subtle my-7 border-t" />
<div>
@ -370,6 +376,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Select
isSearchable={false}
className="text-sm"
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
const oldValue = form.getValues(`steps.${step.stepNumber - 1}.action`);
@ -468,8 +475,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<PhoneInput
placeholder={t("phone_number")}
id={`steps.${step.stepNumber - 1}.sendTo`}
className="min-w-fit sm:rounded-tl-md sm:rounded-bl-md sm:border-r-transparent"
className="min-w-fit sm:rounded-r-none sm:rounded-tl-md sm:rounded-bl-md"
required
disabled={props.readOnly}
value={value}
onChange={(val) => {
const isAlreadyVerified = !!verifiedNumbers
@ -483,9 +491,9 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
/>
<Button
color="secondary"
disabled={numberVerified || false}
disabled={numberVerified || props.readOnly || false}
className={classNames(
"-ml-[3px] h-[40px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none ",
"-ml-[3px] h-[40px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none",
numberVerified ? "hidden" : "mt-3 sm:mt-0"
)}
onClick={() =>
@ -508,38 +516,41 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Badge variant="green">{t("number_verified")}</Badge>
</div>
) : (
<>
<div className="mt-3 flex">
<TextField
className=" border-r-transparent"
placeholder="Verification code"
value={verificationCode}
onChange={(e) => {
setVerificationCode(e.target.value);
}}
required
/>
<Button
color="secondary"
className="-ml-[3px] rounded-tl-none rounded-bl-none "
disabled={verifyPhoneNumberMutation.isLoading}
onClick={() => {
verifyPhoneNumberMutation.mutate({
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
code: verificationCode,
teamId,
});
}}>
{t("verify")}
</Button>
</div>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="mt-1 text-xs text-red-500">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
</>
!props.readOnly && (
<>
<div className="mt-3 flex">
<TextField
className="rounded-r-none border-r-transparent"
placeholder="Verification code"
disabled={props.readOnly}
value={verificationCode}
onChange={(e) => {
setVerificationCode(e.target.value);
}}
required
/>
<Button
color="secondary"
className="-ml-[3px] h-[38px] min-w-fit sm:block sm:rounded-tl-none sm:rounded-bl-none "
disabled={verifyPhoneNumberMutation.isLoading || props.readOnly}
onClick={() => {
verifyPhoneNumberMutation.mutate({
phoneNumber: form.getValues(`steps.${step.stepNumber - 1}.sendTo`) || "",
code: verificationCode,
teamId,
});
}}>
{t("verify")}
</Button>
</div>
{form.formState.errors.steps &&
form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo && (
<p className="mt-1 text-xs text-red-500">
{form.formState?.errors?.steps[step.stepNumber - 1]?.sendTo?.message || ""}
</p>
)}
</>
)
)}
</div>
)}
@ -551,6 +562,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Input
type="text"
placeholder={SENDER_ID}
disabled={props.readOnly}
maxLength={11}
{...form.register(`steps.${step.stepNumber - 1}.sender`)}
/>
@ -566,6 +578,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Label>{t("sender_name")}</Label>
<Input
type="text"
disabled={props.readOnly}
placeholder={SENDER_NAME}
{...form.register(`steps.${step.stepNumber - 1}.senderName`)}
/>
@ -580,6 +593,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
control={form.control}
render={() => (
<Checkbox
disabled={props.readOnly}
defaultChecked={
form.getValues(`steps.${step.stepNumber - 1}.numberRequired`) || false
}
@ -596,6 +610,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<div className="bg-muted mt-5 rounded-md p-4">
<EmailField
required
disabled={props.readOnly}
label={t("email_address")}
{...form.register(`steps.${step.stepNumber - 1}.sendTo`)}
/>
@ -611,6 +626,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
<Select
isSearchable={false}
className="text-sm"
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
if (val.value === WorkflowTemplates.REMINDER) {
@ -657,13 +673,17 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{isEmailSubjectNeeded && (
<div className="mb-6">
<div className="flex items-center">
<Label className="mb-0 flex-none">{t("subject")}</Label>
<div className="flex-grow text-right">
<AddVariablesDropdown
addVariable={addVariableEmailSubject}
variables={DYNAMIC_TEXT_VARIABLES}
/>
</div>
<Label className={classNames("flex-none", props.readOnly ? "mb-2" : "mb-0")}>
{t("subject")}
</Label>
{!props.readOnly && (
<div className="flex-grow text-right">
<AddVariablesDropdown
addVariable={addVariableEmailSubject}
variables={DYNAMIC_TEXT_VARIABLES}
/>
</div>
)}
</div>
<TextArea
ref={(e) => {
@ -671,6 +691,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
refEmailSubject.current = e;
}}
rows={1}
disabled={props.readOnly}
className="my-0 focus:ring-transparent"
required
{...restEmailSubjectForm}
@ -702,6 +723,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
}}
variables={DYNAMIC_TEXT_VARIABLES}
height="200px"
editable={!props.readOnly}
updateTemplate={updateTemplate}
firstRender={firstRender}
setFirstRender={setFirstRender}
@ -710,15 +732,17 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
) : (
<>
<div className="flex items-center">
<Label className="mb-0 flex-none">
<Label className={classNames("flex-none", props.readOnly ? "mb-2" : "mb-0")}>
{isEmailSubjectNeeded ? t("email_body") : t("text_message")}
</Label>
<div className="flex-grow text-right">
<AddVariablesDropdown
addVariable={addVariableBody}
variables={DYNAMIC_TEXT_VARIABLES}
/>
</div>
{!props.readOnly && (
<div className="flex-grow text-right">
<AddVariablesDropdown
addVariable={addVariableBody}
variables={DYNAMIC_TEXT_VARIABLES}
/>
</div>
)}
</div>
<TextArea
ref={(e) => {
@ -726,6 +750,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
refReminderBody.current = e;
}}
className="my-0 h-24"
disabled={props.readOnly}
required
{...restReminderBodyForm}
/>
@ -737,14 +762,16 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
{form.formState?.errors?.steps[step.stepNumber - 1]?.reminderBody?.message || ""}
</p>
)}
<div className="mt-3 ">
<button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}>
<div className="text-default mt-2 flex text-sm">
<HelpCircle className="mt-[3px] h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p className="text-left">{t("using_booking_questions_as_variables")}</p>
</div>
</button>
</div>
{!props.readOnly && (
<div className="mt-3 ">
<button type="button" onClick={() => setIsAdditionalInputsDialogOpen(true)}>
<div className="text-default mt-2 flex text-sm">
<HelpCircle className="mt-[3px] h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p className="text-left">{t("using_booking_questions_as_variables")}</p>
</div>
</button>
</div>
)}
</div>
{/* {form.getValues(`steps.${step.stepNumber - 1}.action`) !== WorkflowActions.SMS_ATTENDEE && (

View File

@ -122,10 +122,13 @@ function WorkflowPage() {
setIsMixedEventType(true);
}
setSelectedEventTypes(
workflow.activeOn.map((active) => ({
value: String(active.eventType.id),
label: active.eventType.title,
})) || []
workflow.activeOn.flatMap((active) => {
if (workflow.teamId && active.eventType.parentId) return [];
return {
value: String(active.eventType.id),
label: active.eventType.title,
};
}) || []
);
const activeOn = workflow.activeOn
? workflow.activeOn.map((active) => ({
@ -252,11 +255,11 @@ function WorkflowPage() {
backPath="/workflows"
title={workflow && workflow.name ? workflow.name : "Untitled"}
CTA={
<div>
<Button type="submit" disabled={readOnly}>
{t("save")}
</Button>
</div>
!readOnly && (
<div>
<Button type="submit">{t("save")}</Button>
</div>
)
}
hideHeadingOnMobile
heading={
@ -271,6 +274,11 @@ function WorkflowPage() {
{workflow.team.slug}
</Badge>
)}
{readOnly && (
<Badge className="mt-1 ml-4" variant="gray">
{t("readonly")}
</Badge>
)}
</div>
)
}>
@ -286,6 +294,7 @@ function WorkflowPage() {
setSelectedEventTypes={setSelectedEventTypes}
teamId={workflow ? workflow.teamId || undefined : undefined}
isMixedEventType={isMixedEventType}
readOnly={readOnly}
/>
</>
) : (

View File

@ -66,6 +66,7 @@ export const ChildrenEventTypeSelect = ({
<div className="flex flex-row items-center gap-3 p-3">
<Avatar
size="mdLg"
className="overflow-visible"
imageSrc={`${CAL_URL}/${children.owner.username}/avatar.png`}
alt={children.owner.name || ""}
/>
@ -73,26 +74,20 @@ export const ChildrenEventTypeSelect = ({
<div className="flex flex-col">
<span className="text text-sm font-semibold leading-none">
{children.owner.name}
{children.owner.membership === MembershipRole.OWNER ? (
<Badge className="ml-2" variant="gray">
{t("owner")}
</Badge>
) : (
<Badge className="ml-2" variant="gray">
{t("member")}
</Badge>
)}
<div className="flex flex-row gap-1">
{children.owner.membership === MembershipRole.OWNER ? (
<Badge variant="gray">{t("owner")}</Badge>
) : (
<Badge variant="gray">{t("member")}</Badge>
)}
{children.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
</div>
</span>
<small className="text-subtle font-normal leading-normal">
{`/${children.owner.username}/${children.slug}`}
</small>
</div>
<div className="flex flex-row items-center gap-2">
{children.hidden && (
<Badge variant="gray" className="hidden sm:block">
{t("hidden")}
</Badge>
)}
<Tooltip content={t("show_eventtype_on_profile")}>
<div className="self-center rounded-md p-2">
<Switch

View File

@ -28,7 +28,7 @@ import {
Select,
SkeletonText,
Switch,
Checkbox
Checkbox,
} from "@calcom/ui";
import { Copy, Plus, Trash } from "@calcom/ui/components/icon";
@ -198,7 +198,9 @@ export const DayRanges = <TFieldValues extends FieldValues>({
}}
/>
)}
{index !== 0 && <RemoveTimeButton index={index} remove={remove} className="text-default mx-2 border-none" />}
{index !== 0 && (
<RemoveTimeButton index={index} remove={remove} className="text-default mx-2 border-none" />
)}
</div>
</Fragment>
))}
@ -388,10 +390,10 @@ const CopyTimes = ({
<ol className="space-y-2">
<li key="select all">
<label className="text-default flex w-full items-center justify-between">
<span className="px-1">{t('select_all')}</span>
<span className="px-1">{t("select_all")}</span>
<Checkbox
description={""}
value={t('select_all')}
description=""
value={t("select_all")}
checked={selected.length === 7}
onChange={(e) => {
if (e.target.checked) {
@ -410,7 +412,7 @@ const CopyTimes = ({
<label className="text-default flex w-full items-center justify-between">
<span className="px-1">{weekday}</span>
<Checkbox
description={""}
description=""
value={weekdayIndex}
checked={selected.includes(weekdayIndex) || disabled === weekdayIndex}
disabled={disabled === weekdayIndex}
@ -418,7 +420,7 @@ const CopyTimes = ({
if (e.target.checked && !selected.includes(weekdayIndex)) {
setSelected(selected.concat([weekdayIndex]));
} else if (!e.target.checked && selected.includes(weekdayIndex)) {
setSelected(selected.filter(item => item !== weekdayIndex));
setSelected(selected.filter((item) => item !== weekdayIndex));
}
}}
/>

View File

@ -103,6 +103,11 @@ export default async function getEventTypeById({
successRedirectUrl: true,
currency: true,
bookingFields: true,
parent: {
select: {
teamId: true,
},
},
team: {
select: {
id: true,
@ -191,6 +196,12 @@ export default async function getEventTypeById({
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},

View File

@ -0,0 +1,9 @@
-- Sanitizing the DB before creating unique index
DELETE FROM "WorkflowsOnEventTypes"
WHERE id not in
(SELECT MIN(id)
FROM "WorkflowsOnEventTypes"
GROUP BY "workflowId", "eventTypeId");
-- CreateIndex
CREATE UNIQUE INDEX "WorkflowsOnEventTypes_workflowId_eventTypeId_key" ON "WorkflowsOnEventTypes"("workflowId", "eventTypeId");

View File

@ -713,6 +713,7 @@ model WorkflowsOnEventTypes {
eventType EventType @relation(fields: [eventTypeId], references: [id], onDelete: Cascade)
eventTypeId Int
@@unique([workflowId, eventTypeId])
@@index([workflowId])
@@index([eventTypeId])
}

View File

@ -46,6 +46,7 @@ export const getByViewerHandler = async ({ ctx }: GetByViewerOptions) => {
users: true,
},
},
parentId: true,
hosts: {
select: {
user: {

View File

@ -248,6 +248,11 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
userId: true,
},
},
workflows: {
select: {
workflowId: true,
},
},
team: {
select: {
name: true,

View File

@ -48,6 +48,9 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
},
],
},
include: {
children: true,
},
});
if (!userEventType)
@ -118,13 +121,15 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
await prisma.workflowsOnEventTypes.deleteMany({
where: {
workflowId,
eventTypeId,
eventTypeId: { in: [eventTypeId].concat(userEventType.children.map((ch) => ch.id)) },
},
});
await removeSmsReminderFieldForBooking({
workflowId,
eventTypeId,
[eventTypeId].concat(userEventType.children.map((ch) => ch.id)).map(async (chId) => {
await removeSmsReminderFieldForBooking({
workflowId,
eventTypeId: chId,
});
});
} else {
// activate workflow and schedule reminders for existing bookings
@ -220,11 +225,13 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
}
}
await prisma.workflowsOnEventTypes.create({
data: {
workflowId,
eventTypeId,
},
await prisma.workflowsOnEventTypes.createMany({
data: [
{
workflowId,
eventTypeId,
},
].concat(userEventType.children.map((ch) => ({ workflowId, eventTypeId: ch.id }))),
});
if (
@ -235,10 +242,12 @@ export const activateEventTypeHandler = async ({ ctx, input }: ActivateEventType
const isSmsReminderNumberRequired = eventTypeWorkflow.steps.some((step) => {
return step.action === WorkflowActions.SMS_ATTENDEE && step.numberRequired;
});
await upsertSmsReminderFieldForBooking({
workflowId,
isSmsReminderNumberRequired,
eventTypeId,
[eventTypeId].concat(userEventType.children.map((ch) => ch.id)).map(async (evTyId) => {
await upsertSmsReminderFieldForBooking({
workflowId,
isSmsReminderNumberRequired,
eventTypeId: evTyId,
});
});
}
}

View File

@ -43,6 +43,12 @@ export const listHandler = async ({ ctx, input }: ListOptions) => {
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},
@ -75,6 +81,12 @@ export const listHandler = async ({ ctx, input }: ListOptions) => {
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},
@ -120,6 +132,12 @@ export const listHandler = async ({ ctx, input }: ListOptions) => {
select: {
id: true,
title: true,
parentId: true,
_count: {
select: {
children: true,
},
},
},
},
},

View File

@ -64,15 +64,46 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
const activeOnEventTypes = await ctx.prisma.eventType.findMany({
where: {
id: {
in: activeOn,
},
},
select: {
id: true,
children: {
select: {
id: true,
},
},
},
});
const activeOnWithChildren = activeOnEventTypes
.map((eventType) => [eventType.id].concat(eventType.children.map((child) => child.id)))
.flat();
const oldActiveOnEventTypes = await ctx.prisma.workflowsOnEventTypes.findMany({
where: {
workflowId: id,
},
select: {
eventTypeId: true,
eventType: {
include: {
children: true,
},
},
},
});
const oldActiveOnEventTypeIds = oldActiveOnEventTypes
.map((eventTypeRel) =>
[eventTypeRel.eventType.id].concat(eventTypeRel.eventType.children.map((child) => child.id))
)
.flat();
const newActiveEventTypes = activeOn.filter((eventType) => {
if (
!oldActiveOnEventTypes ||
@ -99,6 +130,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
members: true,
},
},
children: true,
},
});
@ -119,15 +151,11 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
}
//remove all scheduled Email and SMS reminders for eventTypes that are not active any more
const removedEventTypes = oldActiveOnEventTypes
.map((eventType) => {
return eventType.eventTypeId;
})
.filter((eventType) => {
if (!activeOn.includes(eventType)) {
return eventType;
}
});
const removedEventTypes = oldActiveOnEventTypeIds.filter((eventTypeId) => {
if (!activeOnWithChildren.includes(eventTypeId)) {
return eventTypeId;
}
});
const remindersToDeletePromise: Prisma.PrismaPromise<
{
@ -189,7 +217,16 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
//create reminders for all bookings with newEventTypes
const bookingsForReminders = await ctx.prisma.booking.findMany({
where: {
eventTypeId: { in: newEventTypes },
OR: [
{ eventTypeId: { in: newEventTypes } },
{
eventType: {
parentId: {
in: newEventTypes,
},
},
},
],
status: BookingStatus.ACCEPTED,
startTime: {
gte: new Date(),
@ -288,13 +325,24 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
});
}
//create all workflow - eventtypes relationships
activeOn.forEach(async (eventTypeId) => {
activeOnEventTypes.forEach(async (eventType) => {
await ctx.prisma.workflowsOnEventTypes.createMany({
data: {
workflowId: id,
eventTypeId,
eventTypeId: eventType.id,
},
});
if (eventType.children.length) {
eventType.children.forEach(async (chEventType) => {
await ctx.prisma.workflowsOnEventTypes.createMany({
data: {
workflowId: id,
eventTypeId: chEventType.id,
},
});
});
}
});
}
@ -666,7 +714,7 @@ export const updateHandler = async ({ ctx, input }: UpdateOptions) => {
});
}
for (const eventTypeId of activeOn) {
for (const eventTypeId of activeOnWithChildren) {
if (smsReminderNumberNeeded) {
await upsertSmsReminderFieldForBooking({
workflowId: id,

View File

@ -80,10 +80,16 @@ export const Editor = (props: TextEditorProps) => {
setFirstRender={props.setFirstRender}
/>
<div
className={classNames("editor-inner scroll-bar", !editable && "bg-muted")}
className={classNames("editor-inner scroll-bar", !editable && "!bg-subtle")}
style={{ height: props.height }}>
<RichTextPlugin
contentEditable={<ContentEditable style={{ height: props.height }} className="editor-input" />}
contentEditable={
<ContentEditable
readOnly={!editable}
style={{ height: props.height }}
className="editor-input"
/>
}
placeholder={<div className="text-muted -mt-11 p-3 text-sm">{props.placeholder || ""}</div>}
ErrorBoundary={LexicalErrorBoundary}
/>

View File

@ -55,7 +55,7 @@ const CheckboxField = forwardRef<HTMLInputElement, Props>(
className={classNames(
"text-primary-600 focus:ring-primary-500 border-default bg-default h-4 w-4 rounded ltr:mr-2 rtl:ml-2",
!error && disabled
? "bg-gray-300 checked:bg-gray-300"
? "cursor-not-allowed bg-gray-300 checked:bg-gray-300 hover:bg-gray-300 hover:checked:bg-gray-300"
: "hover:bg-subtle checked:bg-gray-800",
error && "border-red-800 checked:bg-red-800 hover:bg-red-400",
rest.className

View File

@ -65,6 +65,7 @@ export default function MultiSelectCheckboxes({
setSelected,
setValue,
className,
isDisabled,
}: Omit<Props, "options"> & MultiSelectionCheckboxesProps) {
const additonalComponents = { MultiValue };
@ -78,6 +79,7 @@ export default function MultiSelectCheckboxes({
variant="checkbox"
options={options}
isMulti
isDisabled={isDisabled}
className={classNames(className ? className : "w-64 text-sm")}
isSearchable={false}
closeMenuOnSelect={false}

View File

@ -74,7 +74,7 @@ const Addon = ({ isFilled, children, className, error }: AddonProps) => (
)}>
<div
className={classNames(
"flex h-9 flex-col justify-center text-sm",
"min-h-9 flex flex-col justify-center text-sm leading-7",
error ? "text-error" : "text-default"
)}>
<span className="flex whitespace-nowrap">{children}</span>
@ -143,7 +143,7 @@ export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function
isStandaloneField={false}
className={classNames(
className,
"disabled:bg-muted disabled:hover:border-subtle disabled:cursor-not-allowed",
"disabled:bg-subtle disabled:hover:border-subtle disabled:cursor-not-allowed",
addOnLeading && "rounded-l-none border-l-0",
addOnSuffix && "rounded-r-none border-r-0",
type === "search" && "pr-8",
@ -184,7 +184,7 @@ export const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function
placeholder={placeholder}
className={classNames(
className,
"disabled:bg-muted disabled:hover:border-subtle disabled:cursor-not-allowed"
"disabled:bg-subtle disabled:hover:border-subtle disabled:cursor-not-allowed"
)}
{...passThrough}
readOnly={readOnly}
@ -280,7 +280,7 @@ export const TextArea = forwardRef<HTMLTextAreaElement, TextAreaProps>(function
ref={ref}
{...props}
className={classNames(
"hover:border-emphasis border-default bg-default placeholder:text-muted text-emphasis disabled:hover:border-default mb-2 block w-full rounded-md border py-2 px-3 text-sm leading-4 focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1 disabled:cursor-not-allowed",
"hover:border-emphasis border-default bg-default placeholder:text-muted text-emphasis disabled:hover:border-default disabled:bg-subtle mb-2 block w-full rounded-md border py-2 px-3 text-sm focus:border-neutral-300 focus:outline-none focus:ring-2 focus:ring-neutral-800 focus:ring-offset-1 disabled:cursor-not-allowed",
props.className
)}
/>

View File

@ -59,7 +59,7 @@ export const Select = <
? "p-1"
: "px-3 py-2"
: "py-2 px-3",
props.isDisabled && "bg-muted",
props.isDisabled && "bg-subtle",
props.classNames?.control
),
singleValue: () => cx("text-emphasis placeholder:text-muted", props.classNames?.singleValue),