Files
calendar/packages/app-store/routing-forms/components/_components/Header.tsx
T
73adfb0cec fix: header in routing forms (#21445)
* fix header

* remove unused variables

---------

Co-authored-by: Eunjae Lee <hey@eunjae.dev>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
2025-05-22 19:47:10 +00:00

365 lines
12 KiB
TypeScript

"use client";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
import { useFormContext } from "react-hook-form";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import type { RoutingFormWithResponseCount } from "@calcom/routing-forms/types/types";
import { Button } from "@calcom/ui/components/button";
import { DropdownMenuSeparator } from "@calcom/ui/components/dropdown";
import { ToggleGroup } from "@calcom/ui/components/form";
import { Icon } from "@calcom/ui/components/icon";
import { Tooltip } from "@calcom/ui/components/tooltip";
import { FormAction, FormActionsDropdown } from "../FormActions";
import { FormSettingsSlideover } from "./FormSettingsSlideover";
// Toggle group doesnt support HREF navigation, so we need to use this hook to handle navigation
const useRoutingFormNavigation = (
form: RoutingFormWithResponseCount,
appUrl: string,
setShowInfoLostDialog: (value: boolean) => void
) => {
const pathname = usePathname();
const router = useRouter();
const formContext = useFormContext<RoutingFormWithResponseCount>();
// Get the current page based on the pathname since we use a custom routing system
const getCurrentPage = () => {
const path = pathname || "";
if (path.includes("/form-edit/")) return "form-edit";
if (path.includes("/route-builder/")) return "route-builder";
if (path.includes("/incomplete-booking/")) return "incomplete-booking";
return "form-edit"; // default to form-edit if no match
};
const handleNavigation = (value: string) => {
if (!value) return;
const baseUrl = `${appUrl}/${value}/${form.id}`;
if (value === "route-builder" && formContext.formState.isDirty) {
setShowInfoLostDialog(true);
} else {
router.push(baseUrl);
}
};
return {
getCurrentPage,
handleNavigation,
};
};
const Actions = ({
form,
isSaving,
appUrl,
setIsTestPreviewOpen,
isTestPreviewOpen,
isMobile = false,
}: {
form: RoutingFormWithResponseCount;
setIsTestPreviewOpen: (value: boolean) => void;
isSaving: boolean;
appUrl: string;
isTestPreviewOpen: boolean;
isMobile?: boolean;
}) => {
const { t } = useLocale();
const formContext = useFormContext<RoutingFormWithResponseCount>();
const [isSettingsDialogOpen, setIsSettingsDialogOpen] = useState(false);
return (
<>
<div className="flex items-center">
<div className="flex gap-2">
<Tooltip sideOffset={4} content={t("preview")} side="bottom">
<Button
color="secondary"
data-testid={isMobile ? "preview-button-mobile" : "preview-button"}
type="button"
variant="icon"
onClick={() => {
setIsTestPreviewOpen(!isTestPreviewOpen);
}}>
{t("preview")}
</Button>
</Tooltip>
<Tooltip sideOffset={4} content={t("settings")} side="bottom">
<Button
color="secondary"
type="button"
StartIcon="settings"
data-testid={isMobile ? "settings-button-mobile" : "settings-button"}
onClick={() => {
setIsSettingsDialogOpen(true);
}}
/>
</Tooltip>
<FormActionsDropdown>
<FormAction
routingForm={form}
color="minimal"
target="_blank"
type="button"
rel="noreferrer"
action="preview"
StartIcon="external-link">
{t("view_form")}
</FormAction>
<FormAction
action="incompleteBooking"
className="w-full"
routingForm={form}
color="minimal"
type="button"
StartIcon="calendar">
{t("routing_incomplete_booking_tab")}
</FormAction>
<FormAction
action="copyLink"
className="w-full"
routingForm={form}
color="minimal"
type="button"
StartIcon="link">
{t("copy_link_to_form")}
</FormAction>
<FormAction
action="download"
routingForm={form}
className="w-full"
color="minimal"
type="button"
data-testid="download-responses"
StartIcon="download">
{t("download_responses")}
</FormAction>
{form?.id && (
<FormAction
action="viewResponses"
routingForm={form}
className="w-full"
color="minimal"
type="button"
data-testid="view-responses"
StartIcon="eye">
{t("view_responses")}
</FormAction>
)}
<FormAction
action="embed"
routingForm={form}
color="minimal"
type="button"
className="w-full"
StartIcon="code">
{t("embed")}
</FormAction>
<DropdownMenuSeparator className="hidden sm:block" />
<FormAction
action="_delete"
routingForm={form}
className="w-full"
type="button"
color="destructive"
StartIcon="trash">
{t("delete")}
</FormAction>
<div className="block sm:hidden">
<DropdownMenuSeparator />
<FormAction
data-testid="toggle-form"
action="toggle"
routingForm={form}
label="Disable Form"
extraClassNames="hover:bg-subtle cursor-pointer rounded-[5px] pr-4 transition"
/>
</div>
</FormActionsDropdown>
<Button
data-testid={isMobile ? "update-form-mobile" : "update-form"}
loading={isSaving}
type="submit"
color="primary">
{t("save")}
</Button>
</div>
</div>
<FormSettingsSlideover
hookForm={formContext}
form={form}
isOpen={isSettingsDialogOpen}
onOpenChange={setIsSettingsDialogOpen}
appUrl={appUrl}
/>
</>
);
};
export function Header({
routingForm,
isSaving,
appUrl,
setShowInfoLostDialog,
setIsTestPreviewOpen,
isTestPreviewOpen,
}: {
routingForm: RoutingFormWithResponseCount;
isSaving: boolean;
appUrl: string;
setShowInfoLostDialog: (value: boolean) => void;
setIsTestPreviewOpen: (value: boolean) => void;
isTestPreviewOpen: boolean;
}) {
const { t } = useLocale();
const [isEditing, setIsEditing] = useState(false);
const [title, setTitle] = useState(routingForm.name);
const form = useFormContext<RoutingFormWithResponseCount>();
const { getCurrentPage, handleNavigation } = useRoutingFormNavigation(
routingForm,
appUrl,
setShowInfoLostDialog
);
const handleTitleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTitle(e.target.value);
};
const handleTitleSubmit = () => {
setIsEditing(false);
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
form.setValue("name", title);
handleTitleSubmit();
} else if (e.key === "Escape") {
form.setValue("name", routingForm.name);
setIsEditing(false);
}
};
const watchedName = form.watch("name");
return (
<div className="bg-default flex flex-col lg:grid lg:grid-cols-3 lg:items-center">
{/* Left - Back button and title */}
<div className="border-muted flex items-center gap-2 border-b px-4 py-3">
<Button
color="minimal"
variant="icon"
StartIcon="arrow-left"
href={`${appUrl}`}
data-testid="back-button"
/>
<div className="flex min-w-0 items-center">
<span className="text-subtle min-w-content text-sm font-semibold leading-none">
{t("routing_form")}
</span>
<span className="text-subtle mx-1 text-sm font-semibold leading-none">/</span>
{isEditing ? (
<input
{...form.register("name")}
onChange={handleTitleChange}
onKeyDown={handleKeyDown}
onBlur={handleTitleSubmit}
className="text-default h-auto w-full whitespace-nowrap border-none p-0 text-sm font-semibold leading-none focus:ring-0"
autoFocus
/>
) : (
<div className="group flex items-center gap-1">
<span
className="text-default hover:bg-muted min-w-[100px] cursor-pointer truncate whitespace-nowrap rounded px-1 text-sm font-semibold leading-none"
onClick={() => setIsEditing(true)}>
{watchedName || "Loading..."}
</span>
<Button
variant="icon"
color="minimal"
onClick={() => setIsEditing(true)}
CustomStartIcon={
<Icon name="pencil" className="text-subtle group-hover:text-default h-3 w-3" />
}>
<span className="sr-only">Edit</span>
</Button>
</div>
)}
</div>
</div>
{/* Mobile/Tablet layout - Second row with toggle group and actions on the same row */}
<div className="border-muted flex items-center justify-between border-b px-4 py-3 lg:hidden">
{/* Navigation Tabs - Left aligned */}
<div className="flex">
<ToggleGroup
defaultValue={getCurrentPage()}
value={getCurrentPage()}
onValueChange={handleNavigation}
options={[
{
value: "form-edit",
label: t("form"),
iconLeft: <Icon name="menu" className="h-3 w-3" />,
dataTestId: "toggle-group-item-form-edit",
},
{
value: "route-builder",
label: t("routing"),
iconLeft: <Icon name="waypoints" className="h-3 w-3" />,
},
]}
/>
</div>
{/* Actions - Right aligned */}
<div className="flex">
<Actions
form={routingForm}
setIsTestPreviewOpen={setIsTestPreviewOpen}
isTestPreviewOpen={isTestPreviewOpen}
isSaving={isSaving}
appUrl={appUrl}
isMobile={true}
/>
</div>
</div>
{/* Desktop layout - Toggle group in center column */}
<div className="border-muted hidden justify-center border-b px-4 py-3 lg:flex">
<ToggleGroup
defaultValue={getCurrentPage()}
value={getCurrentPage()}
onValueChange={handleNavigation}
options={[
{
value: "form-edit",
label: t("form"),
iconLeft: <Icon name="menu" className="h-3 w-3" />,
},
{
value: "route-builder",
label: t("routing"),
iconLeft: <Icon name="waypoints" className="h-3 w-3" />,
},
]}
/>
</div>
{/* Desktop layout - Actions in right column */}
<div className="border-muted hidden justify-end border-b px-4 py-3 lg:flex">
<Actions
form={routingForm}
setIsTestPreviewOpen={setIsTestPreviewOpen}
isTestPreviewOpen={isTestPreviewOpen}
isSaving={isSaving}
appUrl={appUrl}
/>
</div>
</div>
);
}