fix: github annotations (#6240)

This commit is contained in:
Dhruwang Jariwala
2025-07-22 16:08:34 +05:30
committed by GitHub
parent eee9ee8995
commit e83cfa85a4
47 changed files with 202 additions and 176 deletions

View File

@@ -30,16 +30,16 @@ interface ManageIntegrationProps {
locale: TUserLocale;
}
const tableHeaders = [
"common.survey",
"environments.integrations.airtable.table_name",
"common.questions",
"common.updated_at",
];
export const ManageIntegration = (props: ManageIntegrationProps) => {
const { airtableIntegration, environment, environmentId, setIsConnected, surveys, airtableArray } = props;
const { t } = useTranslate();
const tableHeaders = [
t("common.survey"),
t("environments.integrations.airtable.table_name"),
t("common.questions"),
t("common.updated_at"),
];
const [isDeleting, setisDeleting] = useState(false);
const [isDeleteIntegrationModalOpen, setIsDeleteIntegrationModalOpen] = useState(false);
const [defaultValues, setDefaultValues] = useState<(IntegrationModalInputs & { index: number }) | null>(
@@ -100,7 +100,7 @@ export const ManageIntegration = (props: ManageIntegrationProps) => {
<div className="grid h-12 grid-cols-8 content-center rounded-lg bg-slate-100 text-left text-sm font-semibold text-slate-900">
{tableHeaders.map((header) => (
<div key={header} className={`col-span-2 hidden text-center sm:block`}>
{t(header)}
{header}
</div>
))}
</div>

View File

@@ -281,7 +281,7 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
? `${dateRange?.from ? format(dateRange?.from, "dd LLL") : "Select first date"} - ${
dateRange?.to ? format(dateRange.to, "dd LLL") : "Select last date"
}`
: t(filterRange)}
: filterRange}
</span>
{isFilterDropDownOpen ? (
<ChevronUp className="ml-2 h-4 w-4 opacity-50" />
@@ -296,28 +296,28 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
setFilterRange(getFilterDropDownLabels(t).ALL_TIME);
setDateRange({ from: undefined, to: getTodayDate() });
}}>
<p className="text-slate-700">{t(getFilterDropDownLabels(t).ALL_TIME)}</p>
<p className="text-slate-700">{getFilterDropDownLabels(t).ALL_TIME}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(getFilterDropDownLabels(t).LAST_7_DAYS);
setDateRange({ from: startOfDay(subDays(new Date(), 7)), to: getTodayDate() });
}}>
<p className="text-slate-700">{t(getFilterDropDownLabels(t).LAST_7_DAYS)}</p>
<p className="text-slate-700">{getFilterDropDownLabels(t).LAST_7_DAYS}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(getFilterDropDownLabels(t).LAST_30_DAYS);
setDateRange({ from: startOfDay(subDays(new Date(), 30)), to: getTodayDate() });
}}>
<p className="text-slate-700">{t(getFilterDropDownLabels(t).LAST_30_DAYS)}</p>
<p className="text-slate-700">{getFilterDropDownLabels(t).LAST_30_DAYS}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(getFilterDropDownLabels(t).THIS_MONTH);
setDateRange({ from: startOfMonth(new Date()), to: getTodayDate() });
}}>
<p className="text-slate-700">{t(getFilterDropDownLabels(t).THIS_MONTH)}</p>
<p className="text-slate-700">{getFilterDropDownLabels(t).THIS_MONTH}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
@@ -327,14 +327,14 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
to: endOfMonth(subMonths(getTodayDate(), 1)),
});
}}>
<p className="text-slate-700">{t(getFilterDropDownLabels(t).LAST_MONTH)}</p>
<p className="text-slate-700">{getFilterDropDownLabels(t).LAST_MONTH}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(getFilterDropDownLabels(t).THIS_QUARTER);
setDateRange({ from: startOfQuarter(new Date()), to: endOfQuarter(getTodayDate()) });
}}>
<p className="text-slate-700">{t(getFilterDropDownLabels(t).THIS_QUARTER)}</p>
<p className="text-slate-700">{getFilterDropDownLabels(t).THIS_QUARTER}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
@@ -344,7 +344,7 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
to: endOfQuarter(subQuarters(getTodayDate(), 1)),
});
}}>
<p className="text-slate-700">{t(getFilterDropDownLabels(t).LAST_QUARTER)}</p>
<p className="text-slate-700">{getFilterDropDownLabels(t).LAST_QUARTER}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
@@ -354,14 +354,14 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
to: endOfMonth(getTodayDate()),
});
}}>
<p className="text-slate-700">{t(getFilterDropDownLabels(t).LAST_6_MONTHS)}</p>
<p className="text-slate-700">{getFilterDropDownLabels(t).LAST_6_MONTHS}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
setFilterRange(getFilterDropDownLabels(t).THIS_YEAR);
setDateRange({ from: startOfYear(new Date()), to: endOfYear(getTodayDate()) });
}}>
<p className="text-slate-700">{t(getFilterDropDownLabels(t).THIS_YEAR)}</p>
<p className="text-slate-700">{getFilterDropDownLabels(t).THIS_YEAR}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
@@ -371,7 +371,7 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
to: endOfYear(subYears(getTodayDate(), 1)),
});
}}>
<p className="text-slate-700">{t(getFilterDropDownLabels(t).LAST_YEAR)}</p>
<p className="text-slate-700">{getFilterDropDownLabels(t).LAST_YEAR}</p>
</DropdownMenuItem>
<DropdownMenuItem
onClick={() => {
@@ -380,7 +380,7 @@ export const CustomFilter = ({ survey }: CustomFilterProps) => {
setSelectingDate(DateSelected.FROM);
}}>
<p className="text-sm text-slate-700 hover:ring-0">
{t(getFilterDropDownLabels(t).CUSTOM_RANGE)}
{getFilterDropDownLabels(t).CUSTOM_RANGE}
</p>
</DropdownMenuItem>
</DropdownMenuContent>

View File

@@ -21,8 +21,11 @@ import {
} from "@formbricks/types/surveys/types";
import { TTemplate, TTemplateRole } from "@formbricks/types/templates";
const defaultButtonLabel = "common.next";
const defaultBackButtonLabel = "common.back";
const getDefaultButtonLabel = (label: string | undefined, t: TFnType) =>
createI18nString(label || t("common.next"), []);
const getDefaultBackButtonLabel = (label: string | undefined, t: TFnType) =>
createI18nString(label || t("common.back"), []);
export const buildMultipleChoiceQuestion = ({
id,
@@ -63,8 +66,8 @@ export const buildMultipleChoiceQuestion = ({
const id = containsOther && isLastIndex ? "other" : choiceIds ? choiceIds[index] : createId();
return { id, label: createI18nString(choice, []) };
}),
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
shuffleOption: shuffleOption || "none",
required: required ?? false,
logic,
@@ -103,8 +106,8 @@ export const buildOpenTextQuestion = ({
subheader: subheader ? createI18nString(subheader, []) : undefined,
placeholder: placeholder ? createI18nString(placeholder, []) : undefined,
headline: createI18nString(headline, []),
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
required: required ?? false,
longAnswer,
logic,
@@ -151,8 +154,8 @@ export const buildRatingQuestion = ({
headline: createI18nString(headline, []),
scale,
range,
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
required: required ?? false,
isColorCodingEnabled,
lowerLabel: lowerLabel ? createI18nString(lowerLabel, []) : undefined,
@@ -192,8 +195,8 @@ export const buildNPSQuestion = ({
type: TSurveyQuestionTypeEnum.NPS,
subheader: subheader ? createI18nString(subheader, []) : undefined,
headline: createI18nString(headline, []),
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
required: required ?? false,
isColorCodingEnabled,
lowerLabel: lowerLabel ? createI18nString(lowerLabel, []) : undefined,
@@ -228,8 +231,8 @@ export const buildConsentQuestion = ({
type: TSurveyQuestionTypeEnum.Consent,
subheader: subheader ? createI18nString(subheader, []) : undefined,
headline: createI18nString(headline, []),
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
required: required ?? false,
label: createI18nString(label, []),
logic,
@@ -266,8 +269,8 @@ export const buildCTAQuestion = ({
type: TSurveyQuestionTypeEnum.CTA,
html: html ? createI18nString(html, []) : undefined,
headline: createI18nString(headline, []),
buttonLabel: createI18nString(buttonLabel || t(defaultButtonLabel), []),
backButtonLabel: createI18nString(backButtonLabel || t(defaultBackButtonLabel), []),
buttonLabel: getDefaultButtonLabel(buttonLabel, t),
backButtonLabel: getDefaultBackButtonLabel(backButtonLabel, t),
dismissButtonLabel: dismissButtonLabel ? createI18nString(dismissButtonLabel, []) : undefined,
required: required ?? false,
buttonExternal,

View File

@@ -71,7 +71,7 @@ export const PricingCard = ({
window.open(plan.href, "_blank", "noopener,noreferrer");
}}
className="flex justify-center bg-white">
{t(plan.CTA ?? "common.request_pricing")}
{plan.CTA ?? t("common.request_pricing")}
</Button>
);
}
@@ -88,7 +88,7 @@ export const PricingCard = ({
setLoading(false);
}}
className="flex justify-center">
{t(plan.CTA ?? "common.start_free_trial")}
{plan.CTA ?? t("common.start_free_trial")}
</Button>
);
}
@@ -138,7 +138,7 @@ export const PricingCard = ({
plan.featured ? "text-slate-900" : "text-slate-800",
"text-sm font-semibold leading-6"
)}>
{t(plan.name)}
{plan.name}
</h2>
{isCurrentPlan && (
<Badge type="success" size="normal" text={t("environments.settings.billing.current_plan")} />
@@ -155,7 +155,7 @@ export const PricingCard = ({
? planPeriod === "monthly"
? plan.price.monthly
: plan.price.yearly
: t(plan.price.monthly)}
: plan.price.monthly}
</p>
{plan.id !== projectFeatureKeys.ENTERPRISE && (
<div className="text-sm leading-5">
@@ -196,7 +196,7 @@ export const PricingCard = ({
className={cn(plan.featured ? "text-brand-dark" : "text-slate-500", "h-6 w-5 flex-none")}
aria-hidden="true"
/>
{t(mainFeature)}
{mainFeature}
</li>
))}
</ul>
@@ -215,7 +215,7 @@ export const PricingCard = ({
open={upgradeModalOpen}
setOpen={setUpgradeModalOpen}
text={t("environments.settings.billing.switch_plan_confirmation_text", {
plan: t(plan.name),
plan: plan.name,
price: planPeriod === "monthly" ? plan.price.monthly : plan.price.yearly,
period:
planPeriod === "monthly"

View File

@@ -11,30 +11,30 @@ interface TriggerCheckboxGroupProps {
allowChanges: boolean;
}
const triggers: {
title: string;
value: PipelineTriggers;
}[] = [
{
title: "environments.integrations.webhooks.response_created",
value: "responseCreated",
},
{
title: "environments.integrations.webhooks.response_updated",
value: "responseUpdated",
},
{
title: "environments.integrations.webhooks.response_finished",
value: "responseFinished",
},
];
export const TriggerCheckboxGroup: React.FC<TriggerCheckboxGroupProps> = ({
selectedTriggers,
onCheckboxChange,
allowChanges,
}) => {
const { t } = useTranslate();
const triggers: {
title: string;
value: PipelineTriggers;
}[] = [
{
title: t("environments.integrations.webhooks.response_created"),
value: "responseCreated",
},
{
title: t("environments.integrations.webhooks.response_updated"),
value: "responseUpdated",
},
{
title: t("environments.integrations.webhooks.response_finished"),
value: "responseFinished",
},
];
return (
<div className="mt-1 rounded-lg border border-slate-200">
<div className="grid content-center rounded-lg bg-slate-50 p-3 text-left text-sm text-slate-900">
@@ -58,7 +58,7 @@ export const TriggerCheckboxGroup: React.FC<TriggerCheckboxGroupProps> = ({
}}
disabled={!allowChanges}
/>
<span className="ml-2">{t(trigger.title)}</span>
<span className="ml-2">{trigger.title}</span>
</label>
</div>
))}

View File

@@ -95,6 +95,5 @@ describe("getOrganizationAccessKeyDisplayName", () => {
test("returns tolgee string for other keys", () => {
const t = vi.fn((k) => k);
expect(getOrganizationAccessKeyDisplayName("otherKey", t)).toBe("otherKey");
expect(t).toHaveBeenCalledWith("otherKey");
});
});

View File

@@ -47,6 +47,6 @@ export const getOrganizationAccessKeyDisplayName = (key: string, t: TFnType) =>
case "accessControl":
return t("environments.project.api_keys.access_control");
default:
return t(key);
return key;
}
};

View File

@@ -16,14 +16,6 @@ import { SubmitHandler, useForm } from "react-hook-form";
import toast from "react-hot-toast";
import { z } from "zod";
const placements = [
{ name: "common.bottom_right", value: "bottomRight", disabled: false },
{ name: "common.top_right", value: "topRight", disabled: false },
{ name: "common.top_left", value: "topLeft", disabled: false },
{ name: "common.bottom_left", value: "bottomLeft", disabled: false },
{ name: "common.centered_modal", value: "center", disabled: false },
];
interface EditPlacementProps {
project: Project;
environmentId: string;
@@ -40,6 +32,14 @@ type EditPlacementFormValues = z.infer<typeof ZProjectPlacementInput>;
export const EditPlacementForm = ({ project, isReadOnly }: EditPlacementProps) => {
const { t } = useTranslate();
const placements = [
{ name: t("common.bottom_right"), value: "bottomRight", disabled: false },
{ name: t("common.top_right"), value: "topRight", disabled: false },
{ name: t("common.top_left"), value: "topLeft", disabled: false },
{ name: t("common.bottom_left"), value: "bottomLeft", disabled: false },
{ name: t("common.centered_modal"), value: "center", disabled: false },
];
const form = useForm<EditPlacementFormValues>({
defaultValues: {
placement: project.placement,
@@ -102,7 +102,7 @@ export const EditPlacementForm = ({ project, isReadOnly }: EditPlacementProps) =
<Label
htmlFor={placement.value}
className={`text-slate-900 ${isReadOnly ? "cursor-not-allowed opacity-50" : "cursor-pointer"}`}>
{t(placement.name)}
{placement.name}
</Label>
</div>
))}

View File

@@ -12,16 +12,16 @@ import { RadioGroup, RadioGroupItem } from "@/modules/ui/components/radio-group"
import { Switch } from "@/modules/ui/components/switch";
import { useTranslate } from "@tolgee/react";
const placements = [
{ name: "common.bottom_right", value: "bottomRight", disabled: false },
{ name: "common.top_right", value: "topRight", disabled: false },
{ name: "common.top_left", value: "topLeft", disabled: false },
{ name: "common.bottom_left", value: "bottomLeft", disabled: false },
{ name: "common.centered_modal", value: "center", disabled: false },
];
export const ProjectLookSettingsLoading = () => {
const { t } = useTranslate();
const placements = [
{ name: t("common.bottom_right"), value: "bottomRight", disabled: false },
{ name: t("common.top_right"), value: "topRight", disabled: false },
{ name: t("common.top_left"), value: "topLeft", disabled: false },
{ name: t("common.bottom_left"), value: "bottomLeft", disabled: false },
{ name: t("common.centered_modal"), value: "center", disabled: false },
];
return (
<PageContentWrapper>
<PageHeader pageTitle={t("common.project_configuration")}>
@@ -140,7 +140,7 @@ export const ProjectLookSettingsLoading = () => {
className={cn(
placement.disabled ? "cursor-not-allowed text-slate-500" : "text-slate-900"
)}>
{t(placement.name)}
{placement.name}
</Label>
</div>
))}

View File

@@ -62,7 +62,7 @@ export const TemplateFilters = ({
: "bg-white text-slate-700 hover:bg-slate-100 focus:scale-105 focus:bg-slate-100 focus:outline-none focus:ring-0",
"rounded border border-slate-800 px-2 py-1 text-xs transition-all duration-150"
)}>
{t(filter.label)}
{filter.label}
</button>
))}
</div>

View File

@@ -40,7 +40,7 @@ const getChannelTag = (channels: NonNullabeChannel[] | undefined, t: TFnType): s
const labels = channels
.map((channel) => {
const label = getLabel(channel);
if (label) return t(label);
if (label) return label;
return undefined;
})
.filter((label): label is string => !!label)
@@ -78,12 +78,12 @@ export const TemplateTags = ({ template, selectedFilter }: TemplateTagsProps) =>
// if user selects an industry e.g. eCommerce than the tag should not say "Multiple industries" anymore but "E-Commerce".
if (selectedFilter[1] !== null) {
const industry = getIndustryMapping(t).find((industry) => industry.value === selectedFilter[1]);
if (industry) return t(industry.label);
if (industry) return industry.label;
}
if (!industries || industries.length === 0) return undefined;
return industries.length > 1
? t("environments.surveys.templates.multiple_industries")
: t(getIndustryMapping(t).find((industry) => industry.value === industries[0])?.label ?? "");
: getIndustryMapping(t).find((industry) => industry.value === industries[0])?.label;
};
const industryTag = useMemo(
@@ -93,7 +93,7 @@ export const TemplateTags = ({ template, selectedFilter }: TemplateTagsProps) =>
return (
<div className="flex flex-wrap gap-1.5">
<div className={cn("rounded border px-1.5 py-0.5 text-xs", roleBasedStyling)}>{t(roleTag ?? "")}</div>
<div className={cn("rounded border px-1.5 py-0.5 text-xs", roleBasedStyling)}>{roleTag}</div>
{industryTag && (
<div
className={cn("rounded border border-slate-300 bg-slate-50 px-1.5 py-0.5 text-xs text-slate-500")}>

View File

@@ -42,7 +42,7 @@ export const Placement = ({
<div key={placement.value} className="flex items-center space-x-2 whitespace-nowrap">
<RadioGroupItem id={placement.value} value={placement.value} disabled={placement.disabled} />
<Label htmlFor={placement.value} className="text-slate-900">
{t(placement.name)}
{placement.name}
</Label>
</div>
))}

View File

@@ -166,9 +166,9 @@ export const RecontactOptionsCard = ({
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
/>
<div>
<p className="font-semibold text-slate-700">{t(option.name)}</p>
<p className="font-semibold text-slate-700">{option.name}</p>
<p className="mt-2 text-xs font-normal text-slate-600">{t(option.description)}</p>
<p className="mt-2 text-xs font-normal text-slate-600">{option.description}</p>
</div>
</Label>
{option.id === "displaySome" && localSurvey.displayOption === "displaySome" && (

View File

@@ -1,8 +1,7 @@
import { DropdownMenuItem } from "@/modules/ui/components/dropdown-menu";
import { cleanup, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TSortOption, TSurveyFilters } from "@formbricks/types/surveys/types";
import { TSortOption } from "@formbricks/types/surveys/types";
import { SortOption } from "./sort-option";
// Mock dependencies
@@ -21,7 +20,7 @@ vi.mock("@tolgee/react", () => ({
describe("SortOption", () => {
const mockOption: TSortOption = {
label: "test.sort.option",
value: "testValue",
value: "createdAt",
};
const mockHandleSortChange = vi.fn();
@@ -32,14 +31,14 @@ describe("SortOption", () => {
});
test("renders correctly with the option label", () => {
render(<SortOption option={mockOption} sortBy="otherValue" handleSortChange={mockHandleSortChange} />);
render(<SortOption option={mockOption} sortBy="createdAt" handleSortChange={mockHandleSortChange} />);
expect(screen.getByText("test.sort.option")).toBeInTheDocument();
expect(screen.getByTestId("dropdown-menu-item")).toBeInTheDocument();
});
test("applies correct styling when option is selected", () => {
render(<SortOption option={mockOption} sortBy="testValue" handleSortChange={mockHandleSortChange} />);
render(<SortOption option={mockOption} sortBy="createdAt" handleSortChange={mockHandleSortChange} />);
const circleIndicator = screen.getByTestId("dropdown-menu-item").querySelector("span");
expect(circleIndicator).toHaveClass("bg-brand-dark");
@@ -47,11 +46,10 @@ describe("SortOption", () => {
});
test("applies correct styling when option is not selected", () => {
render(
<SortOption option={mockOption} sortBy="differentValue" handleSortChange={mockHandleSortChange} />
);
render(<SortOption option={mockOption} sortBy="updatedAt" handleSortChange={mockHandleSortChange} />);
const circleIndicator = screen.getByTestId("dropdown-menu-item").querySelector("span");
expect(circleIndicator).toHaveClass("border-white");
expect(circleIndicator).not.toHaveClass("bg-brand-dark");
expect(circleIndicator).not.toHaveClass("outline-brand-dark");
});
@@ -59,7 +57,7 @@ describe("SortOption", () => {
test("calls handleSortChange when clicked", async () => {
const user = userEvent.setup();
render(<SortOption option={mockOption} sortBy="otherValue" handleSortChange={mockHandleSortChange} />);
render(<SortOption option={mockOption} sortBy="createdAt" handleSortChange={mockHandleSortChange} />);
await user.click(screen.getByTestId("dropdown-menu-item"));
expect(mockHandleSortChange).toHaveBeenCalledTimes(1);
@@ -67,7 +65,7 @@ describe("SortOption", () => {
});
test("translates the option label", () => {
render(<SortOption option={mockOption} sortBy="otherValue" handleSortChange={mockHandleSortChange} />);
render(<SortOption option={mockOption} sortBy="createdAt" handleSortChange={mockHandleSortChange} />);
// The mock for useTranslate returns the key itself, so we're checking if translation was attempted
expect(screen.getByText(mockOption.label)).toBeInTheDocument();

View File

@@ -1,7 +1,6 @@
"use client";
import { DropdownMenuItem } from "@/modules/ui/components/dropdown-menu";
import { useTranslate } from "@tolgee/react";
import { TSortOption, TSurveyFilters } from "@formbricks/types/surveys/types";
interface SortOptionProps {
@@ -11,7 +10,6 @@ interface SortOptionProps {
}
export const SortOption = ({ option, sortBy, handleSortChange }: SortOptionProps) => {
const { t } = useTranslate();
return (
<DropdownMenuItem
key={option.label}
@@ -22,7 +20,7 @@ export const SortOption = ({ option, sortBy, handleSortChange }: SortOptionProps
<div className="flex h-full w-full items-center space-x-2 px-2 py-1 hover:bg-slate-700">
<span
className={`h-4 w-4 rounded-full border ${sortBy === option.value ? "bg-brand-dark outline-brand-dark border-slate-900 outline" : "border-white"}`}></span>
<p className="font-normal text-white">{t(option.label)}</p>
<p className="font-normal text-white">{option.label}</p>
</div>
</DropdownMenuItem>
);

View File

@@ -4,11 +4,6 @@ import { afterEach, describe, expect, test, vi } from "vitest";
import { TFilterOption } from "@formbricks/types/surveys/types";
import { SurveyFilterDropdown } from "./survey-filter-dropdown";
// Mock dependencies
vi.mock("@tolgee/react", () => ({
useTranslate: () => ({ t: (key: string) => key }),
}));
// Mock UI components
vi.mock("@/modules/ui/components/checkbox", () => ({
Checkbox: ({ checked, className }) => (

View File

@@ -7,7 +7,6 @@ import {
DropdownMenuItem,
DropdownMenuTrigger,
} from "@/modules/ui/components/dropdown-menu";
import { useTranslate } from "@tolgee/react";
import { ChevronDownIcon } from "lucide-react";
import { TFilterOption } from "@formbricks/types/surveys/types";
@@ -30,7 +29,6 @@ export const SurveyFilterDropdown = ({
isOpen,
toggleDropdown,
}: SurveyFilterDropdownProps) => {
const { t } = useTranslate();
const triggerClasses = `surveyFilterDropdown min-w-auto h-8 rounded-md border border-slate-700 sm:px-2 cursor-pointer outline-none
${selectedOptions.length > 0 ? "bg-slate-900 text-white" : "hover:bg-slate-900"}`;
@@ -56,7 +54,7 @@ export const SurveyFilterDropdown = ({
checked={selectedOptions.includes(option.value)}
className={`bg-white ${selectedOptions.includes(option.value) ? "bg-brand-dark border-none" : ""}`}
/>
<p className="font-normal text-white">{t(option.label)}</p>
<p className="font-normal text-white">{option.label}</p>
</div>
</DropdownMenuItem>
))}

View File

@@ -183,7 +183,7 @@ export const SurveyFilters = ({
<span className="text-sm">
{t("common.sort_by")}:{" "}
{getSortOptions(t).find((option) => option.value === sortBy)
? t(getSortOptions(t).find((option) => option.value === sortBy)?.label ?? "")
? getSortOptions(t).find((option) => option.value === sortBy)?.label
: ""}
</span>
<ChevronDownIcon className="ml-2 h-4 w-4" />

View File

@@ -91,7 +91,7 @@ export const SelectedRowSettings = <T,>({
<>
<div className="bg-primary flex items-center gap-x-2 rounded-md p-1 px-2 text-xs text-white">
<div className="lowercase">
{selectedRowCount} {t(`common.${type}s`)} {t("common.selected")}
{`${selectedRowCount} ${type === "response" ? t("common.responses") : t("common.contacts")} ${t("common.selected")}`}
</div>
<Separator />
<Button
@@ -146,7 +146,7 @@ export const SelectedRowSettings = <T,>({
<DeleteDialog
open={isDeleteDialogOpen}
setOpen={setIsDeleteDialogOpen}
deleteWhat={t(`common.${type}`)}
deleteWhat={type === "response" ? t("common.responses") : t("common.contacts")}
onDelete={handleDelete}
isDeleting={isDeleting}
text={deleteDialogText}

View File

@@ -274,7 +274,7 @@ export const InputCombobox = ({
)}
<CommandList className="m-1">
<CommandEmpty className="mx-2 my-0 text-xs font-semibold text-slate-500">
{emptyDropdownText ? t(emptyDropdownText) : t("environments.surveys.edit.no_option_found")}
{emptyDropdownText ?? t("environments.surveys.edit.no_option_found")}
</CommandEmpty>
{options && options.length > 0 && (
<CommandGroup>

View File

@@ -0,0 +1,18 @@
// packages/js-core/.prettierrc.cjs
const base = require("../config-prettier/prettier-preset");
module.exports = {
...base,
plugins: [
"@trivago/prettier-plugin-sort-imports",
],
importOrder: [
"^vitest$", // 1⃣ vitest first
"<THIRD_PARTY_MODULES>", // 2⃣ then other externals
"^@/.*$", // 3⃣ then anything under @/
"^\\.\\/__mocks__\\/.*$", // 4⃣ then anything under ./__mocks__/
"^[./]", // 5⃣ finally all other relative imports
],
importOrderSortSpecifiers: true,
};

View File

@@ -44,6 +44,7 @@
"author": "Formbricks <hola@formbricks.com>",
"devDependencies": {
"@formbricks/config-typescript": "workspace:*",
"@trivago/prettier-plugin-sort-imports": "5.2.2",
"@formbricks/eslint-config": "workspace:*",
"@vitest/coverage-v8": "3.1.3",
"terser": "5.39.1",

View File

@@ -1,7 +1,7 @@
import { wrapThrowsAsync } from "@/lib/common/utils";
import { ApiResponse, ApiSuccessResponse, CreateOrUpdateUserResponse } from "@/types/api";
import { TEnvironmentState } from "@/types/config";
import { ApiErrorResponse, Result, err, ok } from "@/types/error";
import { type ApiResponse, type ApiSuccessResponse, type CreateOrUpdateUserResponse } from "@/types/api";
import { type TEnvironmentState } from "@/types/config";
import { type ApiErrorResponse, type Result, err, ok } from "@/types/error";
export const makeRequest = async <T>(
appUrl: string,

View File

@@ -1,7 +1,7 @@
// api.test.ts
import { beforeEach, describe, expect, test, vi } from "vitest";
import { ApiClient, makeRequest } from "@/lib/common/api";
import type { TEnvironmentState } from "@/types/config";
import { beforeEach, describe, expect, test, vi } from "vitest";
// Mock fetch
const mockFetch = vi.fn();

View File

@@ -1,8 +1,8 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { CommandQueue, CommandType } from "@/lib/common/command-queue";
import { checkSetup } from "@/lib/common/status";
import { UpdateQueue } from "@/lib/user/update-queue";
import { type Result } from "@/types/error";
import { beforeEach, describe, expect, test, vi } from "vitest";
// Mock the setup module so we can control checkSetup()
vi.mock("@/lib/common/status", () => ({

View File

@@ -1,10 +1,10 @@
/* eslint-disable @typescript-eslint/unbound-method -- required for mocking */
// config.test.ts
import { mockConfig } from "./__mocks__/config.mock";
import { type Mock, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { Config } from "@/lib/common/config";
import { JS_LOCAL_STORAGE_KEY } from "@/lib/common/constants";
import type { TConfig, TConfigUpdateInput } from "@/types/config";
import { type Mock, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { mockConfig } from "./__mocks__/config.mock";
// Define mocks outside of any describe block
const getItemMock = localStorage.getItem as unknown as Mock;

View File

@@ -1,4 +1,5 @@
// event-listeners.test.ts
import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
import {
addCleanupEventListeners,
addEventListeners,
@@ -8,7 +9,6 @@ import {
import * as environmentState from "@/lib/environment/state";
import * as pageUrlEventListeners from "@/lib/survey/no-code-action";
import * as userState from "@/lib/user/state";
import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
// 1) Mock all the imported dependencies

View File

@@ -1,7 +1,7 @@
// file-upload.test.ts
import { beforeEach, describe, expect, test, vi } from "vitest";
import { StorageAPI } from "@/lib/common/file-upload";
import type { TUploadFileConfig } from "@/types/storage";
import { beforeEach, describe, expect, test, vi } from "vitest";
// A global fetch mock so we can capture fetch calls.
// Alternatively, use `vi.stubGlobal("fetch", ...)`.

View File

@@ -1,8 +1,6 @@
// logger.test.ts
import { Logger } from "@/lib/common/logger";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
// adjust import path as needed
import { Logger } from "@/lib/common/logger";
describe("Logger", () => {
let logger: Logger;

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/unbound-method -- required for testing */
import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { Config } from "@/lib/common/config";
import { JS_LOCAL_STORAGE_KEY } from "@/lib/common/constants";
import { addCleanupEventListeners, addEventListeners } from "@/lib/common/event-listeners";
@@ -6,10 +7,10 @@ import { Logger } from "@/lib/common/logger";
import { handleErrorOnFirstSetup, setup, tearDown } from "@/lib/common/setup";
import { setIsSetup } from "@/lib/common/status";
import { filterSurveys, isNowExpired } from "@/lib/common/utils";
import type * as Utils from "@/lib/common/utils";
import { fetchEnvironmentState } from "@/lib/environment/state";
import { DEFAULT_USER_STATE_NO_USER_ID } from "@/lib/user/state";
import { sendUpdatesToBackend } from "@/lib/user/update";
import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
const setItemMock = localStorage.setItem as unknown as Mock;
@@ -50,8 +51,9 @@ vi.mock("@/lib/environment/state", () => ({
// 6) Mock filterSurveys
vi.mock("@/lib/common/utils", async (importOriginal) => {
const originalModule = await importOriginal<typeof Utils>();
return {
...(await importOriginal<typeof import("@/lib/common/utils")>()),
...originalModule,
filterSurveys: vi.fn(),
isNowExpired: vi.fn(),
};

View File

@@ -1,5 +1,5 @@
import { checkSetup, getIsSetup, setIsSetup } from "@/lib/common/status";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { checkSetup, getIsSetup, setIsSetup } from "@/lib/common/status";
describe("checkSetup()", () => {
beforeEach(() => {

View File

@@ -1,5 +1,5 @@
import { TimeoutStack } from "@/lib/common/timeout-stack";
import { afterEach, describe, expect, test, vi } from "vitest";
import { TimeoutStack } from "@/lib/common/timeout-stack";
// Using vitest, we don't need to manually declare globals

View File

@@ -1,4 +1,5 @@
// utils.test.ts
import { beforeEach, describe, expect, test, vi } from "vitest";
import { mockProjectId, mockSurveyId } from "@/lib/common/tests/__mocks__/config.mock";
import {
checkUrlMatch,
@@ -26,7 +27,6 @@ import type {
TUserState,
} from "@/types/config";
import { type TActionClassNoCodeConfig, type TActionClassPageUrlRule } from "@/types/survey";
import { beforeEach, describe, expect, test, vi } from "vitest";
const mockSurveyId1 = "e3kxlpnzmdp84op9qzxl9olj";
const mockSurveyId2 = "qo9rwjmms42hoy3k85fp8vgu";

View File

@@ -1,4 +1,5 @@
// state.test.ts
import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { ApiClient } from "@/lib/common/api";
import { Config } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
@@ -9,7 +10,6 @@ import {
fetchEnvironmentState,
} from "@/lib/environment/state";
import type { TEnvironmentState } from "@/types/config";
import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
// Mock the ApiClient so we can control environment.getEnvironmentState
vi.mock("@/lib/common/api", () => ({

View File

@@ -1,9 +1,9 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { Config } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { trackAction, trackCodeAction, trackNoCodeAction } from "@/lib/survey/action";
import { SurveyStore } from "@/lib/survey/store";
import { triggerSurvey } from "@/lib/survey/widget";
import { beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("@/lib/common/config", () => ({
Config: {

View File

@@ -1,4 +1,5 @@
/* eslint-disable @typescript-eslint/unbound-method -- mock functions are unbound */
import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { Config } from "@/lib/common/config";
import { checkSetup } from "@/lib/common/status";
import { TimeoutStack } from "@/lib/common/timeout-stack";
@@ -17,8 +18,7 @@ import {
removeScrollDepthListener,
} from "@/lib/survey/no-code-action";
import { setIsSurveyRunning } from "@/lib/survey/widget";
import { TActionClassNoCodeConfig } from "@/types/survey";
import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { type TActionClassNoCodeConfig } from "@/types/survey";
vi.mock("@/lib/common/config", () => ({
Config: {

View File

@@ -1,7 +1,7 @@
import { mockSurveyId, mockSurveyName } from "@/lib/survey/tests/__mocks__/store.mock";
import { SurveyStore } from "@/lib/survey/store";
import type { TEnvironmentStateSurvey } from "@/types/config";
import { beforeEach, describe, expect, test, vi } from "vitest";
import { SurveyStore } from "@/lib/survey/store";
import { mockSurveyId, mockSurveyName } from "@/lib/survey/tests/__mocks__/store.mock";
import type { TEnvironmentStateSurvey } from "@/types/config";
describe("SurveyStore", () => {
let store: SurveyStore;

View File

@@ -1,10 +1,10 @@
import { mockSurvey } from "@/lib/survey/tests/__mocks__/widget.mock";
import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { Config } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { filterSurveys, getLanguageCode, shouldDisplayBasedOnPercentage } from "@/lib/common/utils";
import { mockSurvey } from "@/lib/survey/tests/__mocks__/widget.mock";
import * as widget from "@/lib/survey/widget";
import { type TEnvironmentStateSurvey } from "@/types/config";
import { type Mock, type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("@/lib/common/config", () => ({
Config: {

View File

@@ -1,6 +1,6 @@
import { beforeEach, describe, expect, test, vi } from "vitest";
import { setAttributes } from "@/lib/user/attribute";
import { UpdateQueue } from "@/lib/user/update-queue";
import { beforeEach, describe, expect, test, vi } from "vitest";
export const mockAttributes = {
name: "John Doe",

View File

@@ -1,6 +1,6 @@
import { type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { Config } from "@/lib/common/config";
import { addUserStateExpiryCheckListener, clearUserStateExpiryCheckListener } from "@/lib/user/state";
import { type MockInstance, afterEach, beforeEach, describe, expect, test, vi } from "vitest";
const mockUserId = "user_123";

View File

@@ -1,9 +1,9 @@
import { mockAttributes, mockUserId1, mockUserId2 } from "@/lib/user/tests/__mocks__/update-queue.mock";
import { type Mock, type MockInstance, beforeEach, describe, expect, test, vi } from "vitest";
import { Config } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { mockAttributes, mockUserId1, mockUserId2 } from "@/lib/user/tests/__mocks__/update-queue.mock";
import { sendUpdates } from "@/lib/user/update";
import { UpdateQueue } from "@/lib/user/update-queue";
import { type Mock, type MockInstance, beforeEach, describe, expect, test, vi } from "vitest";
// Mock dependencies
vi.mock("@/lib/common/config", () => ({

View File

@@ -1,15 +1,15 @@
import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
import { ApiClient } from "@/lib/common/api";
import { Config } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import {
mockAppUrl,
mockAttributes,
mockEnvironmentId,
mockUserId,
} from "@/lib/user/tests/__mocks__/update.mock";
import { ApiClient } from "@/lib/common/api";
import { Config } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { sendUpdates, sendUpdatesToBackend } from "@/lib/user/update";
import { type TUpdates } from "@/types/config";
import { type Mock, beforeEach, describe, expect, test, vi } from "vitest";
vi.mock("@/lib/common/config", () => ({
Config: {

View File

@@ -1,9 +1,9 @@
import { type Mock, type MockInstance, beforeEach, describe, expect, test, vi } from "vitest";
import { Config } from "@/lib/common/config";
import { Logger } from "@/lib/common/logger";
import { setup, tearDown } from "@/lib/common/setup";
import { UpdateQueue } from "@/lib/user/update-queue";
import { logout, setUserId } from "@/lib/user/user";
import { type Mock, type MockInstance, beforeEach, describe, expect, test, vi } from "vitest";
// Mock dependencies
vi.mock("@/lib/common/config", () => ({

View File

@@ -1,5 +1,5 @@
import { TUserState } from "@/types/config";
import { ApiErrorResponse } from "@/types/error";
import { type TUserState } from "@/types/config";
import { type ApiErrorResponse } from "@/types/error";
export type ApiResponse = ApiSuccessResponse | ApiErrorResponse;

View File

@@ -2,7 +2,7 @@ import { FILE_PICK_EVENT } from "@/lib/constants";
import { getOriginalFileNameFromUrl } from "@/lib/storage";
import { getMimeType, isFulfilled, isRejected } from "@/lib/utils";
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { useEffect, useMemo, useState } from "preact/hooks";
import { useCallback, useEffect, useMemo, useState } from "preact/hooks";
import { type JSXInternal } from "preact/src/jsx";
import { type TAllowedFileExtension } from "@formbricks/types/common";
import { type TJsFileUploadParams } from "@formbricks/types/js";
@@ -36,34 +36,39 @@ export function FileInput({
const [parent] = useAutoAnimate();
// Helper function to filter duplicate files
const filterDuplicateFiles = <T extends { name: string }>(
files: T[],
checkAgainstSelected: boolean = true
): {
filteredFiles: T[];
duplicateFiles: T[];
} => {
const existingFileNames = fileUrls ? fileUrls.map(getOriginalFileNameFromUrl) : [];
const filterDuplicateFiles = useCallback(
<T extends { name: string }>(
files: T[],
checkAgainstSelected: boolean = true
): {
filteredFiles: T[];
duplicateFiles: T[];
} => {
const existingFileNames = fileUrls ? fileUrls.map(getOriginalFileNameFromUrl) : [];
const duplicateFiles = files.filter(
(file) =>
existingFileNames.includes(file.name) ||
(checkAgainstSelected && selectedFiles.some((selectedFile) => selectedFile.name === file.name))
);
const duplicateFiles = files.filter(
(file) =>
existingFileNames.includes(file.name) ||
(checkAgainstSelected && selectedFiles.some((selectedFile) => selectedFile.name === file.name))
);
const filteredFiles = files.filter(
(file) =>
!existingFileNames.includes(file.name) &&
(!checkAgainstSelected || !selectedFiles.some((selectedFile) => selectedFile.name === file.name))
);
const filteredFiles = files.filter(
(file) =>
!existingFileNames.includes(file.name) &&
(!checkAgainstSelected || !selectedFiles.some((selectedFile) => selectedFile.name === file.name))
);
if (duplicateFiles.length > 0) {
const duplicateNames = duplicateFiles.map((file) => file.name).join(", ");
alert(`The following files are already uploaded: ${duplicateNames}. Duplicate files are not allowed.`);
}
if (duplicateFiles.length > 0) {
const duplicateNames = duplicateFiles.map((file) => file.name).join(", ");
alert(
`The following files are already uploaded: ${duplicateNames}. Duplicate files are not allowed.`
);
}
return { filteredFiles, duplicateFiles };
};
return { filteredFiles, duplicateFiles };
},
[fileUrls, selectedFiles]
);
// Listen for the native file-upload event dispatched via window.formbricksSurveys.onFilePick
useEffect(() => {
@@ -131,7 +136,15 @@ export function FileInput({
return () => {
window.removeEventListener(FILE_PICK_EVENT, handleNativeFileUpload as unknown as EventListener);
};
}, [allowedFileExtensions, fileUrls, maxSizeInMB, onFileUpload, onUploadCallback, surveyId]);
}, [
allowedFileExtensions,
fileUrls,
maxSizeInMB,
onFileUpload,
onUploadCallback,
surveyId,
filterDuplicateFiles,
]);
const validateFileSize = async (file: File): Promise<boolean> => {
if (maxSizeInMB) {

View File

@@ -27,7 +27,7 @@ export function ProgressBar({ survey, questionId }: ProgressBarProps) {
const elementIdx = calculateElementIdx(survey, idx, totalCards);
return elementIdx / totalCards;
},
[survey]
[survey, endingCardIds.length]
);
const progressArray = useMemo(() => {

3
pnpm-lock.yaml generated
View File

@@ -675,6 +675,9 @@ importers:
'@formbricks/eslint-config':
specifier: workspace:*
version: link:../config-eslint
'@trivago/prettier-plugin-sort-imports':
specifier: 5.2.2
version: 5.2.2(prettier@3.5.3)
'@vitest/coverage-v8':
specifier: 3.1.3
version: 3.1.3(vitest@3.1.3(@types/node@22.15.18)(jiti@2.4.2)(jsdom@26.1.0)(terser@5.39.1)(tsx@4.19.4)(yaml@2.8.0))