Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 11 additions & 1 deletion packages/features/ee/workflows/components/AddActionDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ interface IAddActionDialog {
needsCredits: boolean;
creditsTeamId?: number;
isOrganization?: boolean;
needsTeamsUpgrade?: boolean;
}[];
}

Expand Down Expand Up @@ -170,7 +171,16 @@ export const AddActionDialog = (props: IAddActionDialog) => {
menuPlacement="bottom"
defaultValue={actionOptions[0]}
onChange={handleSelectAction}
options={actionOptions}
options={actionOptions.map((option) => ({
label: option.label,
value: option.value,
needsTeamsUpgrade: option.needsTeamsUpgrade,
}))}
isOptionDisabled={(option: {
label: string;
value: WorkflowActions;
needsTeamsUpgrade?: boolean;
}) => !!option.needsTeamsUpgrade}
/>
);
}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { Dispatch, SetStateAction } from "react";
import { useState, useEffect } from "react";
import type { UseFormReturn } from "react-hook-form";

import { useHasActiveTeamPlan } from "@calcom/features/billing/hooks/useHasPaidPlan";
import type { WorkflowPermissions } from "@calcom/features/workflows/repositories/WorkflowPermissionsRepository";
import { SENDER_ID, SENDER_NAME, SCANNING_WORKFLOW_STEPS } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
Expand Down Expand Up @@ -43,6 +44,7 @@ export default function WorkflowDetailsPage(props: Props) {
const { form, workflowId, selectedOptions, setSelectedOptions, teamId, isOrg, allOptions, permissions } =
props;
const { t, i18n } = useLocale();
const { hasActiveTeamPlan } = useHasActiveTeamPlan();

const [isAddActionDialogOpen, setIsAddActionDialogOpen] = useState(false);
const [isDeleteStepDialogOpen, setIsDeleteStepDialogOpen] = useState(false);
Expand Down Expand Up @@ -84,12 +86,15 @@ export default function WorkflowDetailsPage(props: Props) {
}
}

const needsTeamsUpgrade = isFormTrigger(form.getValues("trigger")) && !hasActiveTeamPlan;

return {
...option,
label,
creditsTeamId: teamId,
isOrganization: isOrg,
isCalAi: isCalAIAction(option.value),
needsTeamsUpgrade,
Comment on lines +89 to +97
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Consider handling the loading state from useHasActiveTeamPlan.

During the initial load while isPending is true, hasActiveTeamPlan will be falsy, potentially causing a brief flash where upgrade badges appear for users who actually have an active team plan.

Consider destructuring and using isPending to avoid showing upgrade badges during loading:

- const { hasActiveTeamPlan } = useHasActiveTeamPlan();
+ const { hasActiveTeamPlan, isPending } = useHasActiveTeamPlan();
...
- const needsTeamsUpgrade = isFormTrigger(form.getValues("trigger")) && !hasActiveTeamPlan;
+ const needsTeamsUpgrade = isFormTrigger(form.getValues("trigger")) && !isPending && !hasActiveTeamPlan;

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In packages/features/ee/workflows/components/WorkflowDetailsPage.tsx around
lines 89–97, the needsTeamsUpgrade value uses hasActiveTeamPlan which is falsy
while the hook is still loading, causing a flash of upgrade badges; destructure
isPending from useHasActiveTeamPlan and only compute needsTeamsUpgrade when
loading has finished (e.g. set needsTeamsUpgrade = !isPending &&
isFormTrigger(form.getValues("trigger")) && !hasActiveTeamPlan), so while
isPending is true you avoid showing the upgrade badge.

};
})
: [];
Expand Down
36 changes: 25 additions & 11 deletions packages/features/ee/workflows/components/WorkflowStepContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ type WorkflowStepProps = {
creditsTeamId?: number;
isOrganization: boolean;
isCalAi: boolean;
needsTeamsUpgrade?: boolean;
}[];
updateTemplate: boolean;
setUpdateTemplate: Dispatch<SetStateAction<boolean>>;
Expand Down Expand Up @@ -371,14 +372,19 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
handleCreateAgent,
]);

const triggerOptions = getWorkflowTriggerOptions(t);
const triggerOptions = getWorkflowTriggerOptions(t, hasActiveTeamPlan);
const templateOptions = getWorkflowTemplateOptions(t, step?.action, hasActiveTeamPlan, trigger);

const steps = useWatch({
control: form.control,
name: "steps",
});

// Extract current step template from watched steps array
const currentStepTemplate = step ? steps?.[step.stepNumber - 1]?.template : null;

const isReminderTemplate = currentStepTemplate === WorkflowTemplates.REMINDER;

const hasAiAction = hasCalAIAction(steps);
const hasEmailToHostAction = steps.some((s) => s.action === WorkflowActions.EMAIL_HOST);
const hasWhatsappAction = steps.some((s) => isWhatsappAction(s.action));
Expand Down Expand Up @@ -531,11 +537,12 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
isDisabled={props.readOnly}
onChange={(val) => {
if (val) {
const currentTrigger = form.getValues("trigger");
const triggerValue = val.value as WorkflowTriggerEvents;
const currentTrigger = form.getValues("trigger") as WorkflowTriggerEvents;
const isCurrentFormTrigger = isFormTrigger(currentTrigger);
const isNewFormTrigger = isFormTrigger(val.value);
const isNewFormTrigger = isFormTrigger(triggerValue);

form.setValue("trigger", val.value);
form.setValue("trigger", triggerValue);

// Reset activeOn when switching between form and non-form triggers
if (isCurrentFormTrigger !== isNewFormTrigger) {
Expand All @@ -546,12 +553,12 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
form.setValue("selectAll", false);
}

const newTimeSectionText = getTimeSectionText(val.value, t);
const newTimeSectionText = getTimeSectionText(triggerValue, t);
if (newTimeSectionText) {
setTimeSectionText(newTimeSectionText);
if (
val.value === WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW ||
val.value === WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW
triggerValue === WorkflowTriggerEvents.AFTER_HOSTS_CAL_VIDEO_NO_SHOW ||
triggerValue === WorkflowTriggerEvents.AFTER_GUESTS_CAL_VIDEO_NO_SHOW
) {
form.setValue("time", 5);
form.setValue("timeUnit", TimeUnit.MINUTE);
Expand All @@ -564,7 +571,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
form.unregister("time");
form.unregister("timeUnit");
}
if (isFormTrigger(val.value)) {
if (isFormTrigger(triggerValue)) {
const steps = form.getValues("steps");
if (steps?.length) {
const updatedSteps = steps.map((step) =>
Expand All @@ -584,7 +591,14 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
}
}}
defaultValue={selectedTrigger}
options={filteredTriggerOptions}
options={filteredTriggerOptions.map((option) => ({
label: option.label,
value: option.value,
needsTeamsUpgrade: option.needsTeamsUpgrade,
}))}
isOptionDisabled={(option: { label: string; value: string; needsTeamsUpgrade?: boolean }) =>
!!option.needsTeamsUpgrade
}
/>
);
}}
Expand Down Expand Up @@ -1321,7 +1335,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
refEmailSubject.current = e;
}}
rows={2}
disabled={props.readOnly || !hasActiveTeamPlan}
disabled={props.readOnly || (!hasActiveTeamPlan && !isReminderTemplate)}
className="my-0 focus:ring-transparent"
required
{...restEmailSubjectForm}
Expand Down Expand Up @@ -1354,7 +1368,7 @@ export default function WorkflowStepContainer(props: WorkflowStepProps) {
editable={
!props.readOnly &&
!isWhatsappAction(step.action) &&
(hasActiveTeamPlan || isSMSAction(step.action))
(hasActiveTeamPlan || isSMSAction(step.action) || isReminderTemplate)
}
excludedToolbarItems={
!isSMSAction(step.action) ? [] : ["blockType", "bold", "italic", "link"]
Expand Down
13 changes: 10 additions & 3 deletions packages/features/ee/workflows/lib/getOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ export function getWorkflowActionOptions(t: TFunction, isOrgsPlan?: boolean) {
});
}

export function getWorkflowTriggerOptions(t: TFunction) {
export function getWorkflowTriggerOptions(t: TFunction, hasPaidPlan: boolean = false) {
// TODO: remove this after workflows are supported
const filterdWorkflowTriggerEvents = WORKFLOW_TRIGGER_EVENTS.filter(
(event) =>
Expand All @@ -41,8 +41,15 @@ export function getWorkflowTriggerOptions(t: TFunction) {

return filterdWorkflowTriggerEvents.map((triggerEvent) => {
const triggerString = t(`${triggerEvent.toLowerCase()}_trigger`);
const isFormSubmittedTrigger =
triggerEvent === WorkflowTriggerEvents.FORM_SUBMITTED ||
triggerEvent === WorkflowTriggerEvents.FORM_SUBMITTED_NO_EVENT;

return { label: triggerString.charAt(0).toUpperCase() + triggerString.slice(1), value: triggerEvent };
return {
label: triggerString.charAt(0).toUpperCase() + triggerString.slice(1),
value: triggerEvent,
needsTeamsUpgrade: !hasPaidPlan && isFormSubmittedTrigger,
};
});
}

Expand All @@ -55,7 +62,7 @@ function convertToTemplateOptions(
return {
label: t(`${template.toLowerCase()}`),
value: template,
needsTeamsUpgrade: !hasPaidPlan,
needsTeamsUpgrade: !hasPaidPlan && template !== WorkflowTemplates.REMINDER,
} as { label: string; value: any; needsTeamsUpgrade: boolean };
});
}
Expand Down