mirror of
https://github.com/formbricks/formbricks.git
synced 2026-04-19 11:11:05 -05:00
Compare commits
40 Commits
cursor/fix
...
feat-reset
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0878428c79 | ||
|
|
b655e649ac | ||
|
|
ddb95b7cbb | ||
|
|
eced597e8a | ||
|
|
93c72df4d9 | ||
|
|
49560ccba8 | ||
|
|
3f98283d4d | ||
|
|
7b64422a3f | ||
|
|
da72101320 | ||
|
|
5f02ad49c1 | ||
|
|
6644bba6ea | ||
|
|
0b7734f725 | ||
|
|
1536bf6907 | ||
|
|
e81190214f | ||
|
|
48c8906a89 | ||
|
|
717b30115b | ||
|
|
1f3962d2d5 | ||
|
|
619f6e408f | ||
|
|
4a8719abaa | ||
|
|
7b59eb3b26 | ||
|
|
8ac280268d | ||
|
|
34e8f4931d | ||
|
|
ac46850a24 | ||
|
|
6328be220a | ||
|
|
a7ee1f189f | ||
|
|
46a590311b | ||
|
|
0faeffb624 | ||
|
|
d9727a336a | ||
|
|
330e0db668 | ||
|
|
f5b7f73199 | ||
|
|
c02f070307 | ||
|
|
bc489e050a | ||
|
|
3062059ed5 | ||
|
|
f27ede6b2c | ||
|
|
e460ff5100 | ||
|
|
4699c0014b | ||
|
|
52f69be05d | ||
|
|
619c0983a4 | ||
|
|
964fb8d4f4 | ||
|
|
5391c60bba |
@@ -94,6 +94,7 @@ describe("LandingSidebar component", () => {
|
|||||||
organizationId: "o1",
|
organizationId: "o1",
|
||||||
redirect: true,
|
redirect: true,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ export const LandingSidebar = ({
|
|||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
redirect: true,
|
redirect: true,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
icon={<LogOutIcon className="mr-2 h-4 w-4" strokeWidth={1.5} />}>
|
||||||
|
|||||||
@@ -11,22 +11,21 @@ export const ActionClassDataRow = ({
|
|||||||
locale: TUserLocale;
|
locale: TUserLocale;
|
||||||
}) => {
|
}) => {
|
||||||
return (
|
return (
|
||||||
<div className="m-2 grid h-16 grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
<div className="m-2 grid grid-cols-6 content-center rounded-lg transition-colors ease-in-out hover:bg-slate-100">
|
||||||
<div className="col-span-4 flex items-center pl-6 text-sm">
|
<div className="col-span-4 flex items-start py-3 pl-6 text-sm">
|
||||||
<div className="flex items-center">
|
<div className="flex w-full items-center gap-4">
|
||||||
<div className="h-5 w-5 flex-shrink-0 text-slate-500">
|
<div className="mt-1 h-5 w-5 flex-shrink-0 text-slate-500">
|
||||||
{ACTION_TYPE_ICON_LOOKUP[actionClass.type]}
|
{ACTION_TYPE_ICON_LOOKUP[actionClass.type]}
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-4 text-left">
|
<div className="text-left">
|
||||||
<div className="font-medium text-slate-900">{actionClass.name}</div>
|
<div className="break-words font-medium text-slate-900">{actionClass.name}</div>
|
||||||
<div className="text-xs text-slate-400">{actionClass.description}</div>
|
<div className="break-words text-xs text-slate-400">{actionClass.description}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
<div className="col-span-2 my-auto whitespace-nowrap text-center text-sm text-slate-500">
|
||||||
{timeSince(actionClass.createdAt.toString(), locale)}
|
{timeSince(actionClass.createdAt.toString(), locale)}
|
||||||
</div>
|
</div>
|
||||||
<div className="text-center"></div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -221,7 +221,6 @@ describe("MainNavigation", () => {
|
|||||||
vi.mocked(useSignOut).mockReturnValue({ signOut: mockSignOut });
|
vi.mocked(useSignOut).mockReturnValue({ signOut: mockSignOut });
|
||||||
|
|
||||||
// Set up localStorage spy on the mocked localStorage
|
// Set up localStorage spy on the mocked localStorage
|
||||||
const removeItemSpy = vi.spyOn(window.localStorage, "removeItem");
|
|
||||||
|
|
||||||
render(<MainNavigation {...defaultProps} />);
|
render(<MainNavigation {...defaultProps} />);
|
||||||
|
|
||||||
@@ -243,23 +242,18 @@ describe("MainNavigation", () => {
|
|||||||
const logoutButton = screen.getByText("common.logout");
|
const logoutButton = screen.getByText("common.logout");
|
||||||
await userEvent.click(logoutButton);
|
await userEvent.click(logoutButton);
|
||||||
|
|
||||||
// Verify localStorage.removeItem is called with the correct key
|
|
||||||
expect(removeItemSpy).toHaveBeenCalledWith("formbricks-environment-id");
|
|
||||||
|
|
||||||
expect(mockSignOut).toHaveBeenCalledWith({
|
expect(mockSignOut).toHaveBeenCalledWith({
|
||||||
reason: "user_initiated",
|
reason: "user_initiated",
|
||||||
redirectUrl: "/auth/login",
|
redirectUrl: "/auth/login",
|
||||||
organizationId: "org1",
|
organizationId: "org1",
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(mockRouterPush).toHaveBeenCalledWith("/auth/login");
|
expect(mockRouterPush).toHaveBeenCalledWith("/auth/login");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Clean up spy
|
|
||||||
removeItemSpy.mockRestore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles organization switching", async () => {
|
test("handles organization switching", async () => {
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { getLatestStableFbReleaseAction } from "@/app/(app)/environments/[enviro
|
|||||||
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
import { NavigationLink } from "@/app/(app)/environments/[environmentId]/components/NavigationLink";
|
||||||
import FBLogo from "@/images/formbricks-wordmark.svg";
|
import FBLogo from "@/images/formbricks-wordmark.svg";
|
||||||
import { cn } from "@/lib/cn";
|
import { cn } from "@/lib/cn";
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
import { getAccessFlags } from "@/lib/membership/utils";
|
import { getAccessFlags } from "@/lib/membership/utils";
|
||||||
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
import { capitalizeFirstLetter } from "@/lib/utils/strings";
|
||||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||||
@@ -391,14 +390,13 @@ export const MainNavigation = ({
|
|||||||
|
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
|
||||||
|
|
||||||
const route = await signOutWithAudit({
|
const route = await signOutWithAudit({
|
||||||
reason: "user_initiated",
|
reason: "user_initiated",
|
||||||
redirectUrl: "/auth/login",
|
redirectUrl: "/auth/login",
|
||||||
organizationId: organization.id,
|
organizationId: organization.id,
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
router.push(route?.url || "/auth/login"); // NOSONAR // We want to check for empty strings
|
router.push(route?.url || "/auth/login"); // NOSONAR // We want to check for empty strings
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ vi.mock("@/modules/ui/components/switch", () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("../actions", () => ({
|
vi.mock("../actions", () => ({
|
||||||
updateNotificationSettingsAction: vi.fn(() => Promise.resolve()),
|
updateNotificationSettingsAction: vi.fn(() => Promise.resolve({ data: true })),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const surveyId = "survey1";
|
const surveyId = "survey1";
|
||||||
@@ -246,4 +246,204 @@ describe("NotificationSwitch", () => {
|
|||||||
});
|
});
|
||||||
expect(updateNotificationSettingsAction).not.toHaveBeenCalled();
|
expect(updateNotificationSettingsAction).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("shows error toast when updateNotificationSettingsAction fails for 'alert' type", async () => {
|
||||||
|
const mockErrorResponse = { serverError: "Failed to update notification settings" };
|
||||||
|
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
|
||||||
|
|
||||||
|
const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } };
|
||||||
|
renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" });
|
||||||
|
const switchInput = screen.getByLabelText("toggle notification settings for alert");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(switchInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||||
|
notificationSettings: { ...initialSettings, alert: { [surveyId]: true } },
|
||||||
|
});
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("Failed to update notification settings", {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
expect(toast.success).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows error toast when updateNotificationSettingsAction fails for 'weeklySummary' type", async () => {
|
||||||
|
const mockErrorResponse = { serverError: "Database connection failed" };
|
||||||
|
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
|
||||||
|
|
||||||
|
const initialSettings = { ...baseNotificationSettings, weeklySummary: { [projectId]: true } };
|
||||||
|
renderSwitch({
|
||||||
|
surveyOrProjectOrOrganizationId: projectId,
|
||||||
|
notificationSettings: initialSettings,
|
||||||
|
notificationType: "weeklySummary",
|
||||||
|
});
|
||||||
|
const switchInput = screen.getByLabelText("toggle notification settings for weeklySummary");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(switchInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||||
|
notificationSettings: { ...initialSettings, weeklySummary: { [projectId]: false } },
|
||||||
|
});
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("Database connection failed", {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
expect(toast.success).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows error toast when updateNotificationSettingsAction fails for 'unsubscribedOrganizationIds' type", async () => {
|
||||||
|
const mockErrorResponse = { serverError: "Permission denied" };
|
||||||
|
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
|
||||||
|
|
||||||
|
const initialSettings = { ...baseNotificationSettings, unsubscribedOrganizationIds: [] };
|
||||||
|
renderSwitch({
|
||||||
|
surveyOrProjectOrOrganizationId: organizationId,
|
||||||
|
notificationSettings: initialSettings,
|
||||||
|
notificationType: "unsubscribedOrganizationIds",
|
||||||
|
});
|
||||||
|
const switchInput = screen.getByLabelText("toggle notification settings for unsubscribedOrganizationIds");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(switchInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||||
|
notificationSettings: { ...initialSettings, unsubscribedOrganizationIds: [organizationId] },
|
||||||
|
});
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("Permission denied", {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
expect(toast.success).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows error toast when updateNotificationSettingsAction returns null", async () => {
|
||||||
|
const mockErrorResponse = { serverError: "An error occurred" };
|
||||||
|
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
|
||||||
|
|
||||||
|
const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } };
|
||||||
|
renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" });
|
||||||
|
const switchInput = screen.getByLabelText("toggle notification settings for alert");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(switchInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||||
|
notificationSettings: { ...initialSettings, alert: { [surveyId]: true } },
|
||||||
|
});
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("An error occurred", {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
expect(toast.success).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows error toast when updateNotificationSettingsAction returns undefined", async () => {
|
||||||
|
const mockErrorResponse = { serverError: "An error occurred" };
|
||||||
|
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
|
||||||
|
|
||||||
|
const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } };
|
||||||
|
renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" });
|
||||||
|
const switchInput = screen.getByLabelText("toggle notification settings for alert");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(switchInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||||
|
notificationSettings: { ...initialSettings, alert: { [surveyId]: true } },
|
||||||
|
});
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("An error occurred", {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
expect(toast.success).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows error toast when updateNotificationSettingsAction returns response without data property", async () => {
|
||||||
|
const mockErrorResponse = { validationErrors: { _errors: ["Invalid input"] } };
|
||||||
|
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
|
||||||
|
|
||||||
|
const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } };
|
||||||
|
renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" });
|
||||||
|
const switchInput = screen.getByLabelText("toggle notification settings for alert");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(switchInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||||
|
notificationSettings: { ...initialSettings, alert: { [surveyId]: true } },
|
||||||
|
});
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("Invalid input", {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
expect(toast.success).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows error toast when updateNotificationSettingsAction throws an exception", async () => {
|
||||||
|
const mockErrorResponse = { serverError: "Network error" };
|
||||||
|
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
|
||||||
|
|
||||||
|
const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } };
|
||||||
|
renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" });
|
||||||
|
const switchInput = screen.getByLabelText("toggle notification settings for alert");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(switchInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||||
|
notificationSettings: { ...initialSettings, alert: { [surveyId]: true } },
|
||||||
|
});
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("Network error", {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
expect(toast.success).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("switch remains enabled after error occurs", async () => {
|
||||||
|
const mockErrorResponse = { serverError: "Failed to update" };
|
||||||
|
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
|
||||||
|
|
||||||
|
const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } };
|
||||||
|
renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" });
|
||||||
|
const switchInput = screen.getByLabelText("toggle notification settings for alert");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(switchInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("Failed to update", {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
expect(switchInput).toBeEnabled(); // Switch should be re-enabled after error
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows error toast with validation errors for specific fields", async () => {
|
||||||
|
const mockErrorResponse = {
|
||||||
|
validationErrors: {
|
||||||
|
notificationSettings: {
|
||||||
|
_errors: ["Invalid notification settings"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
vi.mocked(updateNotificationSettingsAction).mockResolvedValueOnce(mockErrorResponse);
|
||||||
|
|
||||||
|
const initialSettings = { ...baseNotificationSettings, alert: { [surveyId]: false } };
|
||||||
|
renderSwitch({ notificationSettings: initialSettings, notificationType: "alert" });
|
||||||
|
const switchInput = screen.getByLabelText("toggle notification settings for alert");
|
||||||
|
|
||||||
|
await act(async () => {
|
||||||
|
await user.click(switchInput);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(updateNotificationSettingsAction).toHaveBeenCalledWith({
|
||||||
|
notificationSettings: { ...initialSettings, alert: { [surveyId]: true } },
|
||||||
|
});
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("notificationSettingsInvalid notification settings", {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
expect(toast.success).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { Switch } from "@/modules/ui/components/switch";
|
import { Switch } from "@/modules/ui/components/switch";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||||
@@ -24,6 +26,7 @@ export const NotificationSwitch = ({
|
|||||||
}: NotificationSwitchProps) => {
|
}: NotificationSwitchProps) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
const router = useRouter();
|
||||||
const isChecked =
|
const isChecked =
|
||||||
notificationType === "unsubscribedOrganizationIds"
|
notificationType === "unsubscribedOrganizationIds"
|
||||||
? !notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProjectOrOrganizationId)
|
? !notificationSettings.unsubscribedOrganizationIds?.includes(surveyOrProjectOrOrganizationId)
|
||||||
@@ -50,7 +53,20 @@ export const NotificationSwitch = ({
|
|||||||
!updatedNotificationSettings[notificationType][surveyOrProjectOrOrganizationId];
|
!updatedNotificationSettings[notificationType][surveyOrProjectOrOrganizationId];
|
||||||
}
|
}
|
||||||
|
|
||||||
await updateNotificationSettingsAction({ notificationSettings: updatedNotificationSettings });
|
const updatedNotificationSettingsActionResponse = await updateNotificationSettingsAction({
|
||||||
|
notificationSettings: updatedNotificationSettings,
|
||||||
|
});
|
||||||
|
if (updatedNotificationSettingsActionResponse?.data) {
|
||||||
|
toast.success(t("environments.settings.notifications.notification_settings_updated"), {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
router.refresh();
|
||||||
|
} else {
|
||||||
|
const errorMessage = getFormattedErrorMessage(updatedNotificationSettingsActionResponse);
|
||||||
|
toast.error(errorMessage, {
|
||||||
|
id: "notification-switch",
|
||||||
|
});
|
||||||
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -104,9 +120,6 @@ export const NotificationSwitch = ({
|
|||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onCheckedChange={async () => {
|
onCheckedChange={async () => {
|
||||||
await handleSwitchChange();
|
await handleSwitchChange();
|
||||||
toast.success(t("environments.settings.notifications.notification_settings_updated"), {
|
|
||||||
id: "notification-switch",
|
|
||||||
});
|
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { AuthenticatedActionClientCtx } from "@/lib/utils/action-client/types/co
|
|||||||
import { rateLimit } from "@/lib/utils/rate-limit";
|
import { rateLimit } from "@/lib/utils/rate-limit";
|
||||||
import { updateBrevoCustomer } from "@/modules/auth/lib/brevo";
|
import { updateBrevoCustomer } from "@/modules/auth/lib/brevo";
|
||||||
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
import { withAuditLogging } from "@/modules/ee/audit-logs/lib/handler";
|
||||||
import { sendVerificationNewEmail } from "@/modules/email";
|
import { sendForgotPasswordEmail, sendVerificationNewEmail } from "@/modules/email";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { ZId } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import {
|
import {
|
||||||
@@ -162,3 +162,21 @@ export const removeAvatarAction = authenticatedActionClient.schema(ZRemoveAvatar
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const resetPasswordAction = authenticatedActionClient.action(
|
||||||
|
withAuditLogging(
|
||||||
|
"passwordReset",
|
||||||
|
"user",
|
||||||
|
async ({ ctx }: { ctx: AuthenticatedActionClientCtx; parsedInput: undefined }) => {
|
||||||
|
if (ctx.user.identityProvider !== "email") {
|
||||||
|
throw new OperationNotAllowedError("auth.reset-password.not-allowed");
|
||||||
|
}
|
||||||
|
|
||||||
|
await sendForgotPasswordEmail(ctx.user);
|
||||||
|
|
||||||
|
ctx.auditLoggingCtx.userId = ctx.user.id;
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import userEvent from "@testing-library/user-event";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { TUser } from "@formbricks/types/user";
|
import { TUser } from "@formbricks/types/user";
|
||||||
import { updateUserAction } from "../actions";
|
import { resetPasswordAction, updateUserAction } from "../actions";
|
||||||
import { EditProfileDetailsForm } from "./EditProfileDetailsForm";
|
import { EditProfileDetailsForm } from "./EditProfileDetailsForm";
|
||||||
|
|
||||||
const mockUser = {
|
const mockUser = {
|
||||||
@@ -24,6 +24,8 @@ const mockUser = {
|
|||||||
objective: "other",
|
objective: "other",
|
||||||
} as unknown as TUser;
|
} as unknown as TUser;
|
||||||
|
|
||||||
|
vi.mock("next-auth/react", () => ({ signOut: vi.fn() }));
|
||||||
|
|
||||||
// Mock window.location.reload
|
// Mock window.location.reload
|
||||||
const originalLocation = window.location;
|
const originalLocation = window.location;
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -35,6 +37,11 @@ beforeEach(() => {
|
|||||||
|
|
||||||
vi.mock("@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions", () => ({
|
vi.mock("@/app/(app)/environments/[environmentId]/settings/(account)/profile/actions", () => ({
|
||||||
updateUserAction: vi.fn(),
|
updateUserAction: vi.fn(),
|
||||||
|
resetPasswordAction: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/modules/auth/forgot-password/actions", () => ({
|
||||||
|
forgotPasswordAction: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -50,7 +57,13 @@ describe("EditProfileDetailsForm", () => {
|
|||||||
test("renders with initial user data and updates successfully", async () => {
|
test("renders with initial user data and updates successfully", async () => {
|
||||||
vi.mocked(updateUserAction).mockResolvedValue({ ...mockUser, name: "New Name" } as any);
|
vi.mocked(updateUserAction).mockResolvedValue({ ...mockUser, name: "New Name" } as any);
|
||||||
|
|
||||||
render(<EditProfileDetailsForm user={mockUser} emailVerificationDisabled={true} />);
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={true}
|
||||||
|
isPasswordResetEnabled={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const nameInput = screen.getByPlaceholderText("common.full_name");
|
const nameInput = screen.getByPlaceholderText("common.full_name");
|
||||||
expect(nameInput).toHaveValue(mockUser.name);
|
expect(nameInput).toHaveValue(mockUser.name);
|
||||||
@@ -91,7 +104,13 @@ describe("EditProfileDetailsForm", () => {
|
|||||||
const errorMessage = "Update failed";
|
const errorMessage = "Update failed";
|
||||||
vi.mocked(updateUserAction).mockRejectedValue(new Error(errorMessage));
|
vi.mocked(updateUserAction).mockRejectedValue(new Error(errorMessage));
|
||||||
|
|
||||||
render(<EditProfileDetailsForm user={mockUser} emailVerificationDisabled={false} />);
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={false}
|
||||||
|
isPasswordResetEnabled={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
const nameInput = screen.getByPlaceholderText("common.full_name");
|
const nameInput = screen.getByPlaceholderText("common.full_name");
|
||||||
await userEvent.clear(nameInput);
|
await userEvent.clear(nameInput);
|
||||||
@@ -109,7 +128,13 @@ describe("EditProfileDetailsForm", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("update button is disabled initially and enables on change", async () => {
|
test("update button is disabled initially and enables on change", async () => {
|
||||||
render(<EditProfileDetailsForm user={mockUser} emailVerificationDisabled={false} />);
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={false}
|
||||||
|
isPasswordResetEnabled={false}
|
||||||
|
/>
|
||||||
|
);
|
||||||
const updateButton = screen.getByText("common.update");
|
const updateButton = screen.getByText("common.update");
|
||||||
expect(updateButton).toBeDisabled();
|
expect(updateButton).toBeDisabled();
|
||||||
|
|
||||||
@@ -117,4 +142,68 @@ describe("EditProfileDetailsForm", () => {
|
|||||||
await userEvent.type(nameInput, " updated");
|
await userEvent.type(nameInput, " updated");
|
||||||
expect(updateButton).toBeEnabled();
|
expect(updateButton).toBeEnabled();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("reset password button works", async () => {
|
||||||
|
vi.mocked(resetPasswordAction).mockResolvedValue({ data: { success: true } });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={false}
|
||||||
|
isPasswordResetEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
|
||||||
|
await userEvent.click(resetButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(resetPasswordAction).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(toast.success).toHaveBeenCalledWith("auth.forgot-password.email-sent.heading");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reset password button handles error correctly", async () => {
|
||||||
|
const errorMessage = "Reset failed";
|
||||||
|
vi.mocked(resetPasswordAction).mockResolvedValue({ serverError: errorMessage });
|
||||||
|
|
||||||
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={false}
|
||||||
|
isPasswordResetEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
|
||||||
|
await userEvent.click(resetButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(resetPasswordAction).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(toast.error).toHaveBeenCalledWith(errorMessage);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reset password button shows loading state", async () => {
|
||||||
|
vi.mocked(resetPasswordAction).mockImplementation(() => new Promise(() => {})); // Never resolves
|
||||||
|
|
||||||
|
render(
|
||||||
|
<EditProfileDetailsForm
|
||||||
|
user={mockUser}
|
||||||
|
emailVerificationDisabled={false}
|
||||||
|
isPasswordResetEnabled={true}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const resetButton = screen.getByRole("button", { name: "auth.forgot-password.reset_password" });
|
||||||
|
await userEvent.click(resetButton);
|
||||||
|
|
||||||
|
expect(resetButton).toBeDisabled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
} from "@/modules/ui/components/dropdown-menu";
|
} from "@/modules/ui/components/dropdown-menu";
|
||||||
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
|
import { FormControl, FormError, FormField, FormItem, FormLabel } from "@/modules/ui/components/form";
|
||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
|
import { Label } from "@/modules/ui/components/label";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { ChevronDownIcon } from "lucide-react";
|
import { ChevronDownIcon } from "lucide-react";
|
||||||
@@ -22,7 +23,7 @@ import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
|
|||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { TUser, TUserUpdateInput, ZUser, ZUserEmail } from "@formbricks/types/user";
|
import { TUser, TUserUpdateInput, ZUser, ZUserEmail } from "@formbricks/types/user";
|
||||||
import { updateUserAction } from "../actions";
|
import { resetPasswordAction, updateUserAction } from "../actions";
|
||||||
|
|
||||||
// Schema & types
|
// Schema & types
|
||||||
const ZEditProfileNameFormSchema = ZUser.pick({ name: true, locale: true, email: true }).extend({
|
const ZEditProfileNameFormSchema = ZUser.pick({ name: true, locale: true, email: true }).extend({
|
||||||
@@ -30,13 +31,17 @@ const ZEditProfileNameFormSchema = ZUser.pick({ name: true, locale: true, email:
|
|||||||
});
|
});
|
||||||
type TEditProfileNameForm = z.infer<typeof ZEditProfileNameFormSchema>;
|
type TEditProfileNameForm = z.infer<typeof ZEditProfileNameFormSchema>;
|
||||||
|
|
||||||
|
interface IEditProfileDetailsFormProps {
|
||||||
|
user: TUser;
|
||||||
|
isPasswordResetEnabled?: boolean;
|
||||||
|
emailVerificationDisabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const EditProfileDetailsForm = ({
|
export const EditProfileDetailsForm = ({
|
||||||
user,
|
user,
|
||||||
|
isPasswordResetEnabled,
|
||||||
emailVerificationDisabled,
|
emailVerificationDisabled,
|
||||||
}: {
|
}: IEditProfileDetailsFormProps) => {
|
||||||
user: TUser;
|
|
||||||
emailVerificationDisabled: boolean;
|
|
||||||
}) => {
|
|
||||||
const { t } = useTranslate();
|
const { t } = useTranslate();
|
||||||
|
|
||||||
const form = useForm<TEditProfileNameForm>({
|
const form = useForm<TEditProfileNameForm>({
|
||||||
@@ -50,6 +55,8 @@ export const EditProfileDetailsForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { isSubmitting, isDirty } = form.formState;
|
const { isSubmitting, isDirty } = form.formState;
|
||||||
|
|
||||||
|
const [isResettingPassword, setIsResettingPassword] = useState(false);
|
||||||
const [showModal, setShowModal] = useState(false);
|
const [showModal, setShowModal] = useState(false);
|
||||||
const { signOut: signOutWithAudit } = useSignOut({ id: user.id, email: user.email });
|
const { signOut: signOutWithAudit } = useSignOut({ id: user.id, email: user.email });
|
||||||
|
|
||||||
@@ -90,6 +97,7 @@ export const EditProfileDetailsForm = ({
|
|||||||
redirectUrl: "/email-change-without-verification-success",
|
redirectUrl: "/email-change-without-verification-success",
|
||||||
redirect: true,
|
redirect: true,
|
||||||
callbackUrl: "/email-change-without-verification-success",
|
callbackUrl: "/email-change-without-verification-success",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -121,6 +129,28 @@ export const EditProfileDetailsForm = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleResetPassword = async () => {
|
||||||
|
setIsResettingPassword(true);
|
||||||
|
|
||||||
|
const result = await resetPasswordAction();
|
||||||
|
if (result?.data) {
|
||||||
|
toast.success(t("auth.forgot-password.email-sent.heading"));
|
||||||
|
|
||||||
|
await signOutWithAudit({
|
||||||
|
reason: "password_reset",
|
||||||
|
redirectUrl: "/auth/login",
|
||||||
|
redirect: true,
|
||||||
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
const errorMessage = getFormattedErrorMessage(result);
|
||||||
|
toast.error(t(errorMessage));
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsResettingPassword(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<FormProvider {...form}>
|
<FormProvider {...form}>
|
||||||
@@ -205,6 +235,26 @@ export const EditProfileDetailsForm = ({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
{isPasswordResetEnabled && (
|
||||||
|
<div className="mt-4 space-y-2">
|
||||||
|
<Label htmlFor="reset-password">{t("auth.forgot-password.reset_password")}</Label>
|
||||||
|
<p className="mt-1 text-sm text-slate-500">
|
||||||
|
{t("auth.forgot-password.reset_password_description")}
|
||||||
|
</p>
|
||||||
|
<div className="flex items-center justify-between gap-2">
|
||||||
|
<Input type="email" id="reset-password" defaultValue={user.email} disabled />
|
||||||
|
<Button
|
||||||
|
onClick={handleResetPassword}
|
||||||
|
loading={isResettingPassword}
|
||||||
|
disabled={isResettingPassword}
|
||||||
|
size="default"
|
||||||
|
variant="secondary">
|
||||||
|
{t("auth.forgot-password.reset_password")}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="mt-4"
|
className="mt-4"
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import Page from "./page";
|
|||||||
|
|
||||||
// Mock services and utils
|
// Mock services and utils
|
||||||
vi.mock("@/lib/constants", () => ({
|
vi.mock("@/lib/constants", () => ({
|
||||||
IS_FORMBRICKS_CLOUD: true,
|
IS_FORMBRICKS_CLOUD: 1,
|
||||||
|
PASSWORD_RESET_DISABLED: 1,
|
||||||
EMAIL_VERIFICATION_DISABLED: true,
|
EMAIL_VERIFICATION_DISABLED: true,
|
||||||
}));
|
}));
|
||||||
vi.mock("@/lib/organization/service", () => ({
|
vi.mock("@/lib/organization/service", () => ({
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
import { AccountSettingsNavbar } from "@/app/(app)/environments/[environmentId]/settings/(account)/components/AccountSettingsNavbar";
|
||||||
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
import { AccountSecurity } from "@/app/(app)/environments/[environmentId]/settings/(account)/profile/components/AccountSecurity";
|
||||||
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD } from "@/lib/constants";
|
import { EMAIL_VERIFICATION_DISABLED, IS_FORMBRICKS_CLOUD, PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
||||||
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
import { getOrganizationsWhereUserIsSingleOwner } from "@/lib/organization/service";
|
||||||
import { getUser } from "@/lib/user/service";
|
import { getUser } from "@/lib/user/service";
|
||||||
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
import { getIsMultiOrgEnabled, getIsTwoFactorAuthEnabled } from "@/modules/ee/license-check/lib/utils";
|
||||||
@@ -32,6 +32,8 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
throw new Error(t("common.user_not_found"));
|
throw new Error(t("common.user_not_found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const isPasswordResetEnabled = !PASSWORD_RESET_DISABLED && user.identityProvider === "email";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<PageContentWrapper>
|
<PageContentWrapper>
|
||||||
<PageHeader pageTitle={t("common.account_settings")}>
|
<PageHeader pageTitle={t("common.account_settings")}>
|
||||||
@@ -42,7 +44,11 @@ const Page = async (props: { params: Promise<{ environmentId: string }> }) => {
|
|||||||
<SettingsCard
|
<SettingsCard
|
||||||
title={t("environments.settings.profile.personal_information")}
|
title={t("environments.settings.profile.personal_information")}
|
||||||
description={t("environments.settings.profile.update_personal_info")}>
|
description={t("environments.settings.profile.update_personal_info")}>
|
||||||
<EditProfileDetailsForm emailVerificationDisabled={EMAIL_VERIFICATION_DISABLED} user={user} />
|
<EditProfileDetailsForm
|
||||||
|
user={user}
|
||||||
|
emailVerificationDisabled={EMAIL_VERIFICATION_DISABLED}
|
||||||
|
isPasswordResetEnabled={isPasswordResetEnabled}
|
||||||
|
/>
|
||||||
</SettingsCard>
|
</SettingsCard>
|
||||||
<SettingsCard
|
<SettingsCard
|
||||||
title={t("common.avatar")}
|
title={t("common.avatar")}
|
||||||
|
|||||||
@@ -176,8 +176,8 @@ describe("ShareEmbedSurvey", () => {
|
|||||||
));
|
));
|
||||||
});
|
});
|
||||||
|
|
||||||
test("renders initial 'start' view correctly when open and modalView is 'start'", () => {
|
test("renders initial 'start' view correctly when open and modalView is 'start' for link survey", () => {
|
||||||
render(<ShareEmbedSurvey {...defaultProps} />);
|
render(<ShareEmbedSurvey {...defaultProps} survey={mockSurveyLink} />);
|
||||||
expect(screen.getByText("environments.surveys.summary.your_survey_is_public 🎉")).toBeInTheDocument();
|
expect(screen.getByText("environments.surveys.summary.your_survey_is_public 🎉")).toBeInTheDocument();
|
||||||
expect(screen.getByText("ShareSurveyLinkMock")).toBeInTheDocument();
|
expect(screen.getByText("ShareSurveyLinkMock")).toBeInTheDocument();
|
||||||
expect(screen.getByText("environments.surveys.summary.whats_next")).toBeInTheDocument();
|
expect(screen.getByText("environments.surveys.summary.whats_next")).toBeInTheDocument();
|
||||||
@@ -188,6 +188,18 @@ describe("ShareEmbedSurvey", () => {
|
|||||||
expect(screen.getByTestId("badge-mock")).toHaveTextContent("common.new");
|
expect(screen.getByTestId("badge-mock")).toHaveTextContent("common.new");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("renders initial 'start' view correctly when open and modalView is 'start' for app survey", () => {
|
||||||
|
render(<ShareEmbedSurvey {...defaultProps} survey={mockSurveyWeb} />);
|
||||||
|
// For app surveys, ShareSurveyLink should not be rendered
|
||||||
|
expect(screen.queryByText("ShareSurveyLinkMock")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getByText("environments.surveys.summary.whats_next")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("environments.surveys.summary.embed_survey")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("environments.surveys.summary.configure_alerts")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("environments.surveys.summary.setup_integrations")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("environments.surveys.summary.send_to_panel")).toBeInTheDocument();
|
||||||
|
expect(screen.getByTestId("badge-mock")).toHaveTextContent("common.new");
|
||||||
|
});
|
||||||
|
|
||||||
test("switches to 'embed' view when 'Embed survey' button is clicked", async () => {
|
test("switches to 'embed' view when 'Embed survey' button is clicked", async () => {
|
||||||
render(<ShareEmbedSurvey {...defaultProps} />);
|
render(<ShareEmbedSurvey {...defaultProps} />);
|
||||||
const embedButton = screen.getByText("environments.surveys.summary.embed_survey");
|
const embedButton = screen.getByText("environments.surveys.summary.embed_survey");
|
||||||
@@ -205,7 +217,7 @@ describe("ShareEmbedSurvey", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns to 'start' view when handleInitialPageButton is triggered from EmbedView", async () => {
|
test("returns to 'start' view when handleInitialPageButton is triggered from EmbedView", async () => {
|
||||||
render(<ShareEmbedSurvey {...defaultProps} modalView="embed" />);
|
render(<ShareEmbedSurvey {...defaultProps} survey={mockSurveyLink} modalView="embed" />);
|
||||||
expect(mockEmbedViewComponent).toHaveBeenCalled();
|
expect(mockEmbedViewComponent).toHaveBeenCalled();
|
||||||
expect(screen.getByText("EmbedViewMockContent")).toBeInTheDocument();
|
expect(screen.getByText("EmbedViewMockContent")).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -219,7 +231,7 @@ describe("ShareEmbedSurvey", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns to 'start' view when handleInitialPageButton is triggered from PanelInfoView", async () => {
|
test("returns to 'start' view when handleInitialPageButton is triggered from PanelInfoView", async () => {
|
||||||
render(<ShareEmbedSurvey {...defaultProps} modalView="panel" />);
|
render(<ShareEmbedSurvey {...defaultProps} survey={mockSurveyLink} modalView="panel" />);
|
||||||
expect(mockPanelInfoViewComponent).toHaveBeenCalled();
|
expect(mockPanelInfoViewComponent).toHaveBeenCalled();
|
||||||
expect(screen.getByText("PanelInfoViewMockContent")).toBeInTheDocument();
|
expect(screen.getByText("PanelInfoViewMockContent")).toBeInTheDocument();
|
||||||
|
|
||||||
@@ -257,8 +269,8 @@ describe("ShareEmbedSurvey", () => {
|
|||||||
};
|
};
|
||||||
expect(embedViewProps.tabs.length).toBe(3);
|
expect(embedViewProps.tabs.length).toBe(3);
|
||||||
expect(embedViewProps.tabs.find((tab) => tab.id === "app")).toBeUndefined();
|
expect(embedViewProps.tabs.find((tab) => tab.id === "app")).toBeUndefined();
|
||||||
expect(embedViewProps.tabs[0].id).toBe("email");
|
expect(embedViewProps.tabs[0].id).toBe("link");
|
||||||
expect(embedViewProps.activeId).toBe("email");
|
expect(embedViewProps.activeId).toBe("link");
|
||||||
});
|
});
|
||||||
|
|
||||||
test("correctly configures for 'web' survey type in embed view", () => {
|
test("correctly configures for 'web' survey type in embed view", () => {
|
||||||
|
|||||||
@@ -47,13 +47,14 @@ export const ShareEmbedSurvey = ({
|
|||||||
const tabs = useMemo(
|
const tabs = useMemo(
|
||||||
() =>
|
() =>
|
||||||
[
|
[
|
||||||
{ id: "email", label: t("environments.surveys.summary.embed_in_an_email"), icon: MailIcon },
|
|
||||||
{ id: "webpage", label: t("environments.surveys.summary.embed_on_website"), icon: Code2Icon },
|
|
||||||
{
|
{
|
||||||
id: "link",
|
id: "link",
|
||||||
label: `${isSingleUseLinkSurvey ? t("environments.surveys.summary.single_use_links") : t("environments.surveys.summary.share_the_link")}`,
|
label: `${isSingleUseLinkSurvey ? t("environments.surveys.summary.single_use_links") : t("environments.surveys.summary.share_the_link")}`,
|
||||||
icon: LinkIcon,
|
icon: LinkIcon,
|
||||||
},
|
},
|
||||||
|
{ id: "email", label: t("environments.surveys.summary.embed_in_an_email"), icon: MailIcon },
|
||||||
|
{ id: "webpage", label: t("environments.surveys.summary.embed_on_website"), icon: Code2Icon },
|
||||||
|
|
||||||
{ id: "app", label: t("environments.surveys.summary.embed_in_app"), icon: SmartphoneIcon },
|
{ id: "app", label: t("environments.surveys.summary.embed_in_app"), icon: SmartphoneIcon },
|
||||||
].filter((tab) => !(survey.type === "link" && tab.id === "app")),
|
].filter((tab) => !(survey.type === "link" && tab.id === "app")),
|
||||||
[t, isSingleUseLinkSurvey, survey.type]
|
[t, isSingleUseLinkSurvey, survey.type]
|
||||||
@@ -108,24 +109,26 @@ export const ShareEmbedSurvey = ({
|
|||||||
<Dialog open={open} onOpenChange={handleOpenChange}>
|
<Dialog open={open} onOpenChange={handleOpenChange}>
|
||||||
<DialogContent className="w-full bg-white p-0 lg:h-[700px]" width="wide">
|
<DialogContent className="w-full bg-white p-0 lg:h-[700px]" width="wide">
|
||||||
{showView === "start" ? (
|
{showView === "start" ? (
|
||||||
<div className="h-full max-w-full overflow-hidden">
|
<div className="flex h-full max-w-full flex-col overflow-hidden">
|
||||||
<div className="flex h-[200px] w-full flex-col items-center justify-center space-y-6 p-8 text-center lg:h-2/5">
|
{survey.type === "link" && (
|
||||||
<DialogTitle>
|
<div className="flex h-2/5 w-full flex-col items-center justify-center space-y-6 p-8 text-center">
|
||||||
<p className="pt-2 text-xl font-semibold text-slate-800">
|
<DialogTitle>
|
||||||
{t("environments.surveys.summary.your_survey_is_public")} 🎉
|
<p className="pt-2 text-xl font-semibold text-slate-800">
|
||||||
</p>
|
{t("environments.surveys.summary.your_survey_is_public")} 🎉
|
||||||
</DialogTitle>
|
</p>
|
||||||
<DialogDescription className="hidden" />
|
</DialogTitle>
|
||||||
<ShareSurveyLink
|
<DialogDescription className="hidden" />
|
||||||
survey={survey}
|
<ShareSurveyLink
|
||||||
surveyUrl={surveyUrl}
|
survey={survey}
|
||||||
publicDomain={publicDomain}
|
surveyUrl={surveyUrl}
|
||||||
setSurveyUrl={setSurveyUrl}
|
publicDomain={publicDomain}
|
||||||
locale={user.locale}
|
setSurveyUrl={setSurveyUrl}
|
||||||
/>
|
locale={user.locale}
|
||||||
</div>
|
/>
|
||||||
<div className="flex h-[300px] flex-col items-center justify-center gap-8 rounded-b-lg bg-slate-50 px-8 lg:h-3/5">
|
</div>
|
||||||
<p className="-mt-8 text-sm text-slate-500">{t("environments.surveys.summary.whats_next")}</p>
|
)}
|
||||||
|
<div className="flex h-full flex-col items-center justify-center gap-4 rounded-b-lg bg-slate-50 px-8">
|
||||||
|
<p className="text-sm text-slate-500">{t("environments.surveys.summary.whats_next")}</p>
|
||||||
<div className="grid grid-cols-4 gap-2">
|
<div className="grid grid-cols-4 gap-2">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/modules/ui/components/tooltip";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
import { ChevronDownIcon, ChevronUpIcon } from "lucide-react";
|
||||||
@@ -117,13 +118,13 @@ export const SummaryMetadata = ({
|
|||||||
)}
|
)}
|
||||||
</span>
|
</span>
|
||||||
{!isLoading && (
|
{!isLoading && (
|
||||||
<span className="ml-1 flex items-center rounded-md bg-slate-800 px-2 py-1 text-xs text-slate-50 group-hover:bg-slate-700">
|
<Button variant="secondary" className="h-6 w-6">
|
||||||
{showDropOffs ? (
|
{showDropOffs ? (
|
||||||
<ChevronUpIcon className="h-4 w-4" />
|
<ChevronUpIcon className="h-4 w-4" />
|
||||||
) : (
|
) : (
|
||||||
<ChevronDownIcon className="h-4 w-4" />
|
<ChevronDownIcon className="h-4 w-4" />
|
||||||
)}
|
)}
|
||||||
</span>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -69,13 +69,13 @@ vi.mock("@/modules/survey/hooks/useSingleUseId", () => ({
|
|||||||
|
|
||||||
const mockSearchParams = new URLSearchParams();
|
const mockSearchParams = new URLSearchParams();
|
||||||
const mockPush = vi.fn();
|
const mockPush = vi.fn();
|
||||||
|
const mockReplace = vi.fn();
|
||||||
|
|
||||||
// Mock next/navigation
|
// Mock next/navigation
|
||||||
vi.mock("next/navigation", () => ({
|
vi.mock("next/navigation", () => ({
|
||||||
useRouter: () => ({ push: mockPush }),
|
useRouter: () => ({ push: mockPush, replace: mockReplace }),
|
||||||
useSearchParams: () => mockSearchParams,
|
useSearchParams: () => mockSearchParams,
|
||||||
usePathname: () => "/current",
|
usePathname: () => "/current-path",
|
||||||
useParams: () => ({ environmentId: "env123", surveyId: "survey123" }),
|
|
||||||
}));
|
}));
|
||||||
|
|
||||||
// Mock copySurveyLink to return a predictable string
|
// Mock copySurveyLink to return a predictable string
|
||||||
@@ -131,280 +131,281 @@ const dummySurvey = {
|
|||||||
id: "survey123",
|
id: "survey123",
|
||||||
type: "link",
|
type: "link",
|
||||||
environmentId: "env123",
|
environmentId: "env123",
|
||||||
status: "active",
|
status: "inProgress",
|
||||||
|
resultShareKey: null,
|
||||||
} as unknown as TSurvey;
|
} as unknown as TSurvey;
|
||||||
|
|
||||||
|
const dummyAppSurvey = {
|
||||||
|
id: "survey123",
|
||||||
|
type: "app",
|
||||||
|
environmentId: "env123",
|
||||||
|
status: "inProgress",
|
||||||
|
} as unknown as TSurvey;
|
||||||
|
|
||||||
const dummyEnvironment = { id: "env123", appSetupCompleted: true } as TEnvironment;
|
const dummyEnvironment = { id: "env123", appSetupCompleted: true } as TEnvironment;
|
||||||
const dummyUser = { id: "user123", name: "Test User" } as TUser;
|
const dummyUser = { id: "user123", name: "Test User" } as TUser;
|
||||||
|
|
||||||
describe("SurveyAnalysisCTA - handleCopyLink", () => {
|
describe("SurveyAnalysisCTA", () => {
|
||||||
afterEach(() => {
|
|
||||||
cleanup();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("calls copySurveyLink and clipboard.writeText on success", async () => {
|
|
||||||
render(
|
|
||||||
<SurveyAnalysisCTA
|
|
||||||
survey={dummySurvey}
|
|
||||||
environment={dummyEnvironment}
|
|
||||||
isReadOnly={false}
|
|
||||||
publicDomain={mockPublicDomain}
|
|
||||||
user={dummyUser}
|
|
||||||
responseCount={5}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const copyButton = screen.getByRole("button", { name: "common.copy_link" });
|
|
||||||
fireEvent.click(copyButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(refreshSingleUseIdSpy).toHaveBeenCalled();
|
|
||||||
expect(writeTextMock).toHaveBeenCalledWith("https://public-domain.com/s/survey123?suId=newSingleUseId");
|
|
||||||
expect(toast.success).toHaveBeenCalledWith("common.copied_to_clipboard");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shows error toast on failure", async () => {
|
|
||||||
refreshSingleUseIdSpy.mockImplementationOnce(() => Promise.reject(new Error("fail")));
|
|
||||||
render(
|
|
||||||
<SurveyAnalysisCTA
|
|
||||||
survey={dummySurvey}
|
|
||||||
environment={dummyEnvironment}
|
|
||||||
isReadOnly={false}
|
|
||||||
publicDomain={mockPublicDomain}
|
|
||||||
user={dummyUser}
|
|
||||||
responseCount={5}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
const copyButton = screen.getByRole("button", { name: "common.copy_link" });
|
|
||||||
fireEvent.click(copyButton);
|
|
||||||
|
|
||||||
await waitFor(() => {
|
|
||||||
expect(refreshSingleUseIdSpy).toHaveBeenCalled();
|
|
||||||
expect(writeTextMock).not.toHaveBeenCalled();
|
|
||||||
expect(toast.error).toHaveBeenCalledWith("environments.surveys.summary.failed_to_copy_link");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// New tests for squarePenIcon and edit functionality
|
|
||||||
describe("SurveyAnalysisCTA - Edit functionality", () => {
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.resetAllMocks();
|
vi.resetAllMocks();
|
||||||
|
mockSearchParams.delete("share"); // reset params
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
cleanup();
|
cleanup();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("opens EditPublicSurveyAlertDialog when edit icon is clicked and response count > 0", async () => {
|
describe("Edit functionality", () => {
|
||||||
render(
|
test("opens EditPublicSurveyAlertDialog when edit icon is clicked and response count > 0", async () => {
|
||||||
<SurveyAnalysisCTA
|
render(
|
||||||
survey={dummySurvey}
|
<SurveyAnalysisCTA
|
||||||
environment={dummyEnvironment}
|
survey={dummySurvey}
|
||||||
isReadOnly={false}
|
environment={dummyEnvironment}
|
||||||
publicDomain={mockPublicDomain}
|
isReadOnly={false}
|
||||||
user={dummyUser}
|
publicDomain={mockPublicDomain}
|
||||||
responseCount={5}
|
user={dummyUser}
|
||||||
/>
|
responseCount={5}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
// Find the edit button
|
// Find the edit button
|
||||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||||
await fireEvent.click(editButton);
|
await fireEvent.click(editButton);
|
||||||
|
|
||||||
// Check if dialog is shown
|
// Check if dialog is shown
|
||||||
const dialogTitle = screen.getByText("environments.surveys.edit.caution_edit_published_survey");
|
const dialogTitle = screen.getByText("environments.surveys.edit.caution_edit_published_survey");
|
||||||
expect(dialogTitle).toBeInTheDocument();
|
expect(dialogTitle).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("navigates directly to edit page when response count = 0", async () => {
|
||||||
|
render(
|
||||||
|
<SurveyAnalysisCTA
|
||||||
|
survey={dummySurvey}
|
||||||
|
environment={dummyEnvironment}
|
||||||
|
isReadOnly={false}
|
||||||
|
publicDomain={mockPublicDomain}
|
||||||
|
user={dummyUser}
|
||||||
|
responseCount={0}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the edit button
|
||||||
|
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||||
|
await fireEvent.click(editButton);
|
||||||
|
|
||||||
|
// Should navigate directly to edit page
|
||||||
|
expect(mockPush).toHaveBeenCalledWith(
|
||||||
|
`/environments/${dummyEnvironment.id}/surveys/${dummySurvey.id}/edit`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("doesn't show edit button when isReadOnly is true", () => {
|
||||||
|
render(
|
||||||
|
<SurveyAnalysisCTA
|
||||||
|
survey={dummySurvey}
|
||||||
|
environment={dummyEnvironment}
|
||||||
|
isReadOnly={true}
|
||||||
|
publicDomain={mockPublicDomain}
|
||||||
|
user={dummyUser}
|
||||||
|
responseCount={5}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const editButton = screen.queryByRole("button", { name: "common.edit" });
|
||||||
|
expect(editButton).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("navigates directly to edit page when response count = 0", async () => {
|
describe("Duplicate functionality", () => {
|
||||||
render(
|
test("duplicates survey and redirects on primary button click", async () => {
|
||||||
<SurveyAnalysisCTA
|
mockCopySurveyToOtherEnvironmentAction.mockResolvedValue({
|
||||||
survey={dummySurvey}
|
data: { id: "newSurvey456" },
|
||||||
environment={dummyEnvironment}
|
});
|
||||||
isReadOnly={false}
|
|
||||||
publicDomain={mockPublicDomain}
|
|
||||||
user={dummyUser}
|
|
||||||
responseCount={0}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Find the edit button
|
render(
|
||||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
<SurveyAnalysisCTA
|
||||||
await fireEvent.click(editButton);
|
survey={dummySurvey}
|
||||||
|
environment={dummyEnvironment}
|
||||||
|
isReadOnly={false}
|
||||||
|
publicDomain={mockPublicDomain}
|
||||||
|
user={dummyUser}
|
||||||
|
responseCount={5}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
// Should navigate directly to edit page
|
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||||
expect(mockPush).toHaveBeenCalledWith(
|
fireEvent.click(editButton);
|
||||||
`/environments/${dummyEnvironment.id}/surveys/${dummySurvey.id}/edit`
|
|
||||||
);
|
const primaryButton = await screen.findByText("environments.surveys.edit.caution_edit_duplicate");
|
||||||
|
fireEvent.click(primaryButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(mockCopySurveyToOtherEnvironmentAction).toHaveBeenCalledWith({
|
||||||
|
environmentId: "env123",
|
||||||
|
surveyId: "survey123",
|
||||||
|
targetEnvironmentId: "env123",
|
||||||
|
});
|
||||||
|
expect(mockPush).toHaveBeenCalledWith("/environments/env123/surveys/newSurvey456/edit");
|
||||||
|
expect(toast.success).toHaveBeenCalledWith("environments.surveys.survey_duplicated_successfully");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("shows error toast on duplication failure", async () => {
|
||||||
|
const error = { error: "Duplication failed" };
|
||||||
|
mockCopySurveyToOtherEnvironmentAction.mockResolvedValue(error);
|
||||||
|
render(
|
||||||
|
<SurveyAnalysisCTA
|
||||||
|
survey={dummySurvey}
|
||||||
|
environment={dummyEnvironment}
|
||||||
|
isReadOnly={false}
|
||||||
|
publicDomain={mockPublicDomain}
|
||||||
|
user={dummyUser}
|
||||||
|
responseCount={5}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const editButton = screen.getByRole("button", { name: "common.edit" });
|
||||||
|
fireEvent.click(editButton);
|
||||||
|
|
||||||
|
const primaryButton = await screen.findByText("environments.surveys.edit.caution_edit_duplicate");
|
||||||
|
fireEvent.click(primaryButton);
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(toast.error).toHaveBeenCalledWith("Duplication failed");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("doesn't show edit button when isReadOnly is true", () => {
|
describe("Share button and modal", () => {
|
||||||
render(
|
test("opens share modal when 'Share survey' button is clicked", async () => {
|
||||||
<SurveyAnalysisCTA
|
render(
|
||||||
survey={dummySurvey}
|
<SurveyAnalysisCTA
|
||||||
environment={dummyEnvironment}
|
survey={dummySurvey}
|
||||||
isReadOnly={true}
|
environment={dummyEnvironment}
|
||||||
publicDomain={mockPublicDomain}
|
isReadOnly={false}
|
||||||
user={dummyUser}
|
publicDomain={mockPublicDomain}
|
||||||
responseCount={5}
|
user={dummyUser}
|
||||||
/>
|
responseCount={5}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
// Try to find the edit button (it shouldn't exist)
|
const shareButton = screen.getByText("environments.surveys.summary.share_survey");
|
||||||
const editButton = screen.queryByRole("button", { name: "common.edit" });
|
fireEvent.click(shareButton);
|
||||||
expect(editButton).not.toBeInTheDocument();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Updated test description to mention EditPublicSurveyAlertDialog
|
// The share button opens the embed modal, not a URL
|
||||||
describe("SurveyAnalysisCTA - duplicateSurveyAndRoute and EditPublicSurveyAlertDialog", () => {
|
// We can verify this by checking that the ShareEmbedSurvey component is rendered
|
||||||
afterEach(() => {
|
// with the embed modal open
|
||||||
cleanup();
|
expect(screen.getByText("environments.surveys.summary.share_survey")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("renders ShareEmbedSurvey component when share modal is open", async () => {
|
||||||
|
mockSearchParams.set("share", "true");
|
||||||
|
render(
|
||||||
|
<SurveyAnalysisCTA
|
||||||
|
survey={dummySurvey}
|
||||||
|
environment={dummyEnvironment}
|
||||||
|
isReadOnly={false}
|
||||||
|
publicDomain={mockPublicDomain}
|
||||||
|
user={dummyUser}
|
||||||
|
responseCount={5}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
// Assuming ShareEmbedSurvey renders a dialog with a specific title when open
|
||||||
|
const dialog = await screen.findByRole("dialog");
|
||||||
|
expect(dialog).toBeInTheDocument();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("duplicates survey successfully and navigates to edit page", async () => {
|
describe("General UI and visibility", () => {
|
||||||
// Mock the API response
|
test("shows public results badge when resultShareKey is present", () => {
|
||||||
mockCopySurveyToOtherEnvironmentAction.mockResolvedValueOnce({
|
const surveyWithShareKey = { ...dummySurvey, resultShareKey: "someKey" } as TSurvey;
|
||||||
data: { id: "duplicated-survey-456" },
|
render(
|
||||||
|
<SurveyAnalysisCTA
|
||||||
|
survey={surveyWithShareKey}
|
||||||
|
environment={dummyEnvironment}
|
||||||
|
isReadOnly={false}
|
||||||
|
publicDomain={mockPublicDomain}
|
||||||
|
user={dummyUser}
|
||||||
|
responseCount={5}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.getByText("environments.surveys.summary.results_are_public")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
render(
|
test("shows SurveyStatusDropdown for non-draft surveys", () => {
|
||||||
<SurveyAnalysisCTA
|
render(
|
||||||
survey={dummySurvey}
|
<SurveyAnalysisCTA
|
||||||
environment={dummyEnvironment}
|
survey={dummySurvey}
|
||||||
isReadOnly={false}
|
environment={dummyEnvironment}
|
||||||
publicDomain={mockPublicDomain}
|
isReadOnly={false}
|
||||||
user={dummyUser}
|
publicDomain={mockPublicDomain}
|
||||||
responseCount={5}
|
user={dummyUser}
|
||||||
/>
|
responseCount={5}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
// Find and click the edit button to show dialog
|
expect(screen.getByRole("combobox")).toBeInTheDocument();
|
||||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
|
||||||
await fireEvent.click(editButton);
|
|
||||||
|
|
||||||
// Find and click the duplicate button in dialog
|
|
||||||
const duplicateButton = screen.getByRole("button", {
|
|
||||||
name: "environments.surveys.edit.caution_edit_duplicate",
|
|
||||||
});
|
|
||||||
await fireEvent.click(duplicateButton);
|
|
||||||
|
|
||||||
// Verify the API was called with correct parameters
|
|
||||||
expect(mockCopySurveyToOtherEnvironmentAction).toHaveBeenCalledWith({
|
|
||||||
environmentId: dummyEnvironment.id,
|
|
||||||
surveyId: dummySurvey.id,
|
|
||||||
targetEnvironmentId: dummyEnvironment.id,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Verify success toast was shown
|
test("does not show SurveyStatusDropdown for draft surveys", () => {
|
||||||
expect(toast.success).toHaveBeenCalledWith("environments.surveys.survey_duplicated_successfully");
|
const draftSurvey = { ...dummySurvey, status: "draft" } as TSurvey;
|
||||||
|
render(
|
||||||
// Verify navigation to edit page
|
<SurveyAnalysisCTA
|
||||||
expect(mockPush).toHaveBeenCalledWith(
|
survey={draftSurvey}
|
||||||
`/environments/${dummyEnvironment.id}/surveys/duplicated-survey-456/edit`
|
environment={dummyEnvironment}
|
||||||
);
|
isReadOnly={false}
|
||||||
});
|
publicDomain={mockPublicDomain}
|
||||||
|
user={dummyUser}
|
||||||
test("shows error toast when duplication fails with error object", async () => {
|
responseCount={5}
|
||||||
// Mock API failure with error object
|
/>
|
||||||
mockCopySurveyToOtherEnvironmentAction.mockResolvedValueOnce({
|
);
|
||||||
error: "Test error message",
|
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
render(
|
test("hides status dropdown and edit actions when isReadOnly is true", () => {
|
||||||
<SurveyAnalysisCTA
|
render(
|
||||||
survey={dummySurvey}
|
<SurveyAnalysisCTA
|
||||||
environment={dummyEnvironment}
|
survey={dummySurvey}
|
||||||
isReadOnly={false}
|
environment={dummyEnvironment}
|
||||||
publicDomain={mockPublicDomain}
|
isReadOnly={true}
|
||||||
user={dummyUser}
|
publicDomain={mockPublicDomain}
|
||||||
responseCount={5}
|
user={dummyUser}
|
||||||
/>
|
responseCount={5}
|
||||||
);
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
// Open dialog
|
expect(screen.queryByRole("combobox")).not.toBeInTheDocument();
|
||||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
expect(screen.queryByRole("button", { name: "common.edit" })).not.toBeInTheDocument();
|
||||||
await fireEvent.click(editButton);
|
|
||||||
|
|
||||||
// Click duplicate
|
|
||||||
const duplicateButton = screen.getByRole("button", {
|
|
||||||
name: "environments.surveys.edit.caution_edit_duplicate",
|
|
||||||
});
|
|
||||||
await fireEvent.click(duplicateButton);
|
|
||||||
|
|
||||||
// Verify error toast
|
|
||||||
expect(toast.error).toHaveBeenCalledWith("Test error message");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("navigates to edit page when cancel button is clicked in dialog", async () => {
|
|
||||||
render(
|
|
||||||
<SurveyAnalysisCTA
|
|
||||||
survey={dummySurvey}
|
|
||||||
environment={dummyEnvironment}
|
|
||||||
isReadOnly={false}
|
|
||||||
publicDomain={mockPublicDomain}
|
|
||||||
user={dummyUser}
|
|
||||||
responseCount={5}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
// Open dialog
|
|
||||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
|
||||||
await fireEvent.click(editButton);
|
|
||||||
|
|
||||||
// Click edit (cancel) button
|
|
||||||
const editButtonInDialog = screen.getByRole("button", { name: "common.edit" });
|
|
||||||
await fireEvent.click(editButtonInDialog);
|
|
||||||
|
|
||||||
// Verify navigation
|
|
||||||
expect(mockPush).toHaveBeenCalledWith(
|
|
||||||
`/environments/${dummyEnvironment.id}/surveys/${dummySurvey.id}/edit`
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("shows loading state when duplicating survey", async () => {
|
|
||||||
// Create a promise that we can resolve manually
|
|
||||||
let resolvePromise: (value: any) => void;
|
|
||||||
const promise = new Promise((resolve) => {
|
|
||||||
resolvePromise = resolve;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
mockCopySurveyToOtherEnvironmentAction.mockImplementation(() => promise);
|
test("shows preview button for link surveys", () => {
|
||||||
|
render(
|
||||||
render(
|
<SurveyAnalysisCTA
|
||||||
<SurveyAnalysisCTA
|
survey={dummySurvey}
|
||||||
survey={dummySurvey}
|
environment={dummyEnvironment}
|
||||||
environment={dummyEnvironment}
|
isReadOnly={false}
|
||||||
isReadOnly={false}
|
publicDomain={mockPublicDomain}
|
||||||
publicDomain={mockPublicDomain}
|
user={dummyUser}
|
||||||
user={dummyUser}
|
responseCount={5}
|
||||||
responseCount={5}
|
/>
|
||||||
/>
|
);
|
||||||
);
|
expect(screen.getByRole("button", { name: "common.preview" })).toBeInTheDocument();
|
||||||
|
|
||||||
// Open dialog
|
|
||||||
const editButton = screen.getByRole("button", { name: "common.edit" });
|
|
||||||
await fireEvent.click(editButton);
|
|
||||||
|
|
||||||
// Click duplicate
|
|
||||||
const duplicateButton = screen.getByRole("button", {
|
|
||||||
name: "environments.surveys.edit.caution_edit_duplicate",
|
|
||||||
});
|
|
||||||
await fireEvent.click(duplicateButton);
|
|
||||||
|
|
||||||
// Button should now be in loading state
|
|
||||||
// expect(duplicateButton).toHaveAttribute("data-state", "loading");
|
|
||||||
|
|
||||||
// Resolve the promise
|
|
||||||
resolvePromise!({
|
|
||||||
data: { id: "duplicated-survey-456" },
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Wait for the promise to resolve
|
test("hides preview button for app surveys", () => {
|
||||||
await waitFor(() => {
|
render(
|
||||||
expect(mockPush).toHaveBeenCalled();
|
<SurveyAnalysisCTA
|
||||||
|
survey={dummyAppSurvey}
|
||||||
|
environment={dummyEnvironment}
|
||||||
|
isReadOnly={false}
|
||||||
|
publicDomain={mockPublicDomain}
|
||||||
|
user={dummyUser}
|
||||||
|
responseCount={5}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
expect(screen.queryByRole("button", { name: "common.preview" })).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,13 +5,12 @@ import { SuccessMessage } from "@/app/(app)/environments/[environmentId]/surveys
|
|||||||
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
import { SurveyStatusDropdown } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/SurveyStatusDropdown";
|
||||||
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
import { getFormattedErrorMessage } from "@/lib/utils/helper";
|
||||||
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
|
import { EditPublicSurveyAlertDialog } from "@/modules/survey/components/edit-public-survey-alert-dialog";
|
||||||
import { useSingleUseId } from "@/modules/survey/hooks/useSingleUseId";
|
|
||||||
import { copySurveyLink } from "@/modules/survey/lib/client-utils";
|
|
||||||
import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions";
|
import { copySurveyToOtherEnvironmentAction } from "@/modules/survey/list/actions";
|
||||||
import { Badge } from "@/modules/ui/components/badge";
|
import { Badge } from "@/modules/ui/components/badge";
|
||||||
|
import { Button } from "@/modules/ui/components/button";
|
||||||
import { IconBar } from "@/modules/ui/components/iconbar";
|
import { IconBar } from "@/modules/ui/components/iconbar";
|
||||||
import { useTranslate } from "@tolgee/react";
|
import { useTranslate } from "@tolgee/react";
|
||||||
import { BellRing, Code2Icon, Eye, LinkIcon, SquarePenIcon, UsersRound } from "lucide-react";
|
import { BellRing, Eye, SquarePenIcon } from "lucide-react";
|
||||||
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
import { usePathname, useRouter, useSearchParams } from "next/navigation";
|
||||||
import { useEffect, useMemo, useState } from "react";
|
import { useEffect, useMemo, useState } from "react";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
@@ -57,7 +56,6 @@ export const SurveyAnalysisCTA = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const surveyUrl = useMemo(() => `${publicDomain}/s/${survey.id}`, [survey.id, publicDomain]);
|
const surveyUrl = useMemo(() => `${publicDomain}/s/${survey.id}`, [survey.id, publicDomain]);
|
||||||
const { refreshSingleUseId } = useSingleUseId(survey);
|
|
||||||
|
|
||||||
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
const widgetSetupCompleted = survey.type === "app" && environment.appSetupCompleted;
|
||||||
|
|
||||||
@@ -79,22 +77,6 @@ export const SurveyAnalysisCTA = ({
|
|||||||
setModalState((prev) => ({ ...prev, share: open }));
|
setModalState((prev) => ({ ...prev, share: open }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleCopyLink = () => {
|
|
||||||
refreshSingleUseId()
|
|
||||||
.then((newId) => {
|
|
||||||
const linkToCopy = copySurveyLink(surveyUrl, newId);
|
|
||||||
return navigator.clipboard.writeText(linkToCopy);
|
|
||||||
})
|
|
||||||
.then(() => {
|
|
||||||
toast.success(t("common.copied_to_clipboard"));
|
|
||||||
})
|
|
||||||
.catch((err) => {
|
|
||||||
toast.error(t("environments.surveys.summary.failed_to_copy_link"));
|
|
||||||
console.error(err);
|
|
||||||
});
|
|
||||||
setModalState((prev) => ({ ...prev, dropdown: false }));
|
|
||||||
};
|
|
||||||
|
|
||||||
const duplicateSurveyAndRoute = async (surveyId: string) => {
|
const duplicateSurveyAndRoute = async (surveyId: string) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
|
const duplicatedSurveyResponse = await copySurveyToOtherEnvironmentAction({
|
||||||
@@ -134,24 +116,6 @@ export const SurveyAnalysisCTA = ({
|
|||||||
const [isCautionDialogOpen, setIsCautionDialogOpen] = useState(false);
|
const [isCautionDialogOpen, setIsCautionDialogOpen] = useState(false);
|
||||||
|
|
||||||
const iconActions = [
|
const iconActions = [
|
||||||
{
|
|
||||||
icon: Eye,
|
|
||||||
tooltip: t("common.preview"),
|
|
||||||
onClick: () => window.open(getPreviewUrl(), "_blank"),
|
|
||||||
isVisible: survey.type === "link",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: LinkIcon,
|
|
||||||
tooltip: t("common.copy_link"),
|
|
||||||
onClick: handleCopyLink,
|
|
||||||
isVisible: survey.type === "link",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
icon: Code2Icon,
|
|
||||||
tooltip: t("common.embed"),
|
|
||||||
onClick: () => handleModalState("embed")(true),
|
|
||||||
isVisible: !isReadOnly,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
icon: BellRing,
|
icon: BellRing,
|
||||||
tooltip: t("environments.surveys.summary.configure_alerts"),
|
tooltip: t("environments.surveys.summary.configure_alerts"),
|
||||||
@@ -159,13 +123,10 @@ export const SurveyAnalysisCTA = ({
|
|||||||
isVisible: !isReadOnly,
|
isVisible: !isReadOnly,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: UsersRound,
|
icon: Eye,
|
||||||
tooltip: t("environments.surveys.summary.send_to_panel"),
|
tooltip: t("common.preview"),
|
||||||
onClick: () => {
|
onClick: () => window.open(getPreviewUrl(), "_blank"),
|
||||||
handleModalState("panel")(true);
|
isVisible: survey.type === "link",
|
||||||
setModalState((prev) => ({ ...prev, dropdown: false }));
|
|
||||||
},
|
|
||||||
isVisible: !isReadOnly,
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: SquarePenIcon,
|
icon: SquarePenIcon,
|
||||||
@@ -195,6 +156,13 @@ export const SurveyAnalysisCTA = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<IconBar actions={iconActions} />
|
<IconBar actions={iconActions} />
|
||||||
|
<Button
|
||||||
|
className="h-10"
|
||||||
|
onClick={() => {
|
||||||
|
setModalState((prev) => ({ ...prev, embed: true }));
|
||||||
|
}}>
|
||||||
|
{t("environments.surveys.summary.share_survey")}
|
||||||
|
</Button>
|
||||||
|
|
||||||
{user && (
|
{user && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -186,6 +186,18 @@ describe("Response Lib Tests", () => {
|
|||||||
expect(logger.error).not.toHaveBeenCalled(); // Should be caught and re-thrown as DatabaseError
|
expect(logger.error).not.toHaveBeenCalled(); // Should be caught and re-thrown as DatabaseError
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("should handle RelatedRecordDoesNotExist error with specific message", async () => {
|
||||||
|
const prismaError = new Prisma.PrismaClientKnownRequestError("Related record does not exist", {
|
||||||
|
code: "P2025", // PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
|
clientVersion: "2.0",
|
||||||
|
});
|
||||||
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization);
|
||||||
|
vi.mocked(prisma.response.create).mockRejectedValue(prismaError);
|
||||||
|
|
||||||
|
await expect(createResponse(mockResponseInput)).rejects.toThrow(DatabaseError);
|
||||||
|
await expect(createResponse(mockResponseInput)).rejects.toThrow("Display ID does not exist");
|
||||||
|
});
|
||||||
|
|
||||||
test("should handle generic errors", async () => {
|
test("should handle generic errors", async () => {
|
||||||
const genericError = new Error("Something went wrong");
|
const genericError = new Error("Something went wrong");
|
||||||
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization);
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(mockOrganization);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { validateInputs } from "@/lib/utils/validate";
|
|||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
import { ZId, ZOptionalNumber } from "@formbricks/types/common";
|
import { ZId, ZOptionalNumber } from "@formbricks/types/common";
|
||||||
import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
import { TContactAttributes } from "@formbricks/types/contact-attribute";
|
||||||
@@ -176,6 +177,9 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
|
|||||||
return response;
|
return response;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (error.code === PrismaErrorType.RelatedRecordDoesNotExist) {
|
||||||
|
throw new DatabaseError("Display ID does not exist");
|
||||||
|
}
|
||||||
throw new DatabaseError(error.message);
|
throw new DatabaseError(error.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -149,6 +149,10 @@ export const POST = withApiLogging(
|
|||||||
return {
|
return {
|
||||||
response: responses.badRequestResponse(error.message),
|
response: responses.badRequestResponse(error.message),
|
||||||
};
|
};
|
||||||
|
} else if (error instanceof DatabaseError) {
|
||||||
|
return {
|
||||||
|
response: responses.badRequestResponse(error.message),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
logger.error({ error, url: request.url }, "Error in POST /api/v1/management/responses");
|
logger.error({ error, url: request.url }, "Error in POST /api/v1/management/responses");
|
||||||
return {
|
return {
|
||||||
@@ -158,7 +162,7 @@ export const POST = withApiLogging(
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof DatabaseError) {
|
if (error instanceof DatabaseError) {
|
||||||
return {
|
return {
|
||||||
response: responses.badRequestResponse(error.message),
|
response: responses.badRequestResponse("An unexpected error occurred while creating the response"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -65,7 +65,8 @@ export const validateSingleFile = (
|
|||||||
return !allowedFileExtensions || allowedFileExtensions.includes(extension as TAllowedFileExtension);
|
return !allowedFileExtensions || allowedFileExtensions.includes(extension as TAllowedFileExtension);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const validateFileUploads = (data: TResponseData, questions?: TSurveyQuestion[]): boolean => {
|
export const validateFileUploads = (data?: TResponseData, questions?: TSurveyQuestion[]): boolean => {
|
||||||
|
if (!data) return true;
|
||||||
for (const key of Object.keys(data)) {
|
for (const key of Object.keys(data)) {
|
||||||
const question = questions?.find((q) => q.id === key);
|
const question = questions?.find((q) => q.id === key);
|
||||||
if (!question || question.type !== TSurveyQuestionTypeEnum.FileUpload) continue;
|
if (!question || question.type !== TSurveyQuestionTypeEnum.FileUpload) continue;
|
||||||
|
|||||||
@@ -1,9 +1,16 @@
|
|||||||
import { BILLING_LIMITS, PROJECT_FEATURE_KEYS } from "@/lib/constants";
|
import { BILLING_LIMITS, PROJECT_FEATURE_KEYS } from "@/lib/constants";
|
||||||
|
import { updateUser } from "@/lib/user/service";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { DatabaseError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
import { createOrganization, getOrganization, getOrganizationsByUserId, updateOrganization } from "./service";
|
import {
|
||||||
|
createOrganization,
|
||||||
|
getOrganization,
|
||||||
|
getOrganizationsByUserId,
|
||||||
|
subscribeOrganizationMembersToSurveyResponses,
|
||||||
|
updateOrganization,
|
||||||
|
} from "./service";
|
||||||
|
|
||||||
vi.mock("@formbricks/database", () => ({
|
vi.mock("@formbricks/database", () => ({
|
||||||
prisma: {
|
prisma: {
|
||||||
@@ -13,9 +20,16 @@ vi.mock("@formbricks/database", () => ({
|
|||||||
create: vi.fn(),
|
create: vi.fn(),
|
||||||
update: vi.fn(),
|
update: vi.fn(),
|
||||||
},
|
},
|
||||||
|
user: {
|
||||||
|
findUnique: vi.fn(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
vi.mock("@/lib/user/service", () => ({
|
||||||
|
updateUser: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe("Organization Service", () => {
|
describe("Organization Service", () => {
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
@@ -252,4 +266,62 @@ describe("Organization Service", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("subscribeOrganizationMembersToSurveyResponses", () => {
|
||||||
|
test("should subscribe user to survey responses when not unsubscribed", async () => {
|
||||||
|
const mockUser = {
|
||||||
|
id: "user-123",
|
||||||
|
notificationSettings: {
|
||||||
|
alert: { "existing-survey-id": true },
|
||||||
|
weeklySummary: {},
|
||||||
|
unsubscribedOrganizationIds: [], // User is subscribed to all organizations
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const surveyId = "survey-123";
|
||||||
|
const userId = "user-123";
|
||||||
|
const organizationId = "org-123";
|
||||||
|
|
||||||
|
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce(mockUser);
|
||||||
|
vi.mocked(updateUser).mockResolvedValueOnce({} as any);
|
||||||
|
|
||||||
|
await subscribeOrganizationMembersToSurveyResponses(surveyId, userId, organizationId);
|
||||||
|
|
||||||
|
expect(prisma.user.findUnique).toHaveBeenCalledWith({
|
||||||
|
where: { id: userId },
|
||||||
|
});
|
||||||
|
expect(updateUser).toHaveBeenCalledWith(userId, {
|
||||||
|
notificationSettings: {
|
||||||
|
alert: {
|
||||||
|
"existing-survey-id": true,
|
||||||
|
"survey-123": true,
|
||||||
|
},
|
||||||
|
weeklySummary: {},
|
||||||
|
unsubscribedOrganizationIds: [],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should not subscribe user when unsubscribed from organization", async () => {
|
||||||
|
const mockUser = {
|
||||||
|
id: "user-123",
|
||||||
|
notificationSettings: {
|
||||||
|
alert: { "existing-survey-id": true },
|
||||||
|
weeklySummary: {},
|
||||||
|
unsubscribedOrganizationIds: ["org-123"], // User has unsubscribed from this organization
|
||||||
|
},
|
||||||
|
} as any;
|
||||||
|
|
||||||
|
const surveyId = "survey-123";
|
||||||
|
const userId = "user-123";
|
||||||
|
const organizationId = "org-123";
|
||||||
|
|
||||||
|
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce(mockUser);
|
||||||
|
|
||||||
|
await subscribeOrganizationMembersToSurveyResponses(surveyId, userId, organizationId);
|
||||||
|
|
||||||
|
// Should not call updateUser because user is unsubscribed from this organization
|
||||||
|
expect(updateUser).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "Du kannst Dich jetzt mit deinem neuen Passwort einloggen"
|
"text": "Du kannst Dich jetzt mit deinem neuen Passwort einloggen"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "Passwort zurücksetzen"
|
"reset_password": "Passwort zurücksetzen",
|
||||||
|
"reset_password_description": "Du wirst abgemeldet, um dein Passwort zurückzusetzen."
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "Konto erstellen",
|
"create_account": "Konto erstellen",
|
||||||
@@ -191,7 +192,6 @@
|
|||||||
"e_commerce": "E-Commerce",
|
"e_commerce": "E-Commerce",
|
||||||
"edit": "Bearbeiten",
|
"edit": "Bearbeiten",
|
||||||
"email": "E-Mail",
|
"email": "E-Mail",
|
||||||
"embed": "Einbetten",
|
|
||||||
"enterprise_license": "Enterprise Lizenz",
|
"enterprise_license": "Enterprise Lizenz",
|
||||||
"environment_not_found": "Umgebung nicht gefunden",
|
"environment_not_found": "Umgebung nicht gefunden",
|
||||||
"environment_notice": "Du befindest dich derzeit in der {environment}-Umgebung.",
|
"environment_notice": "Du befindest dich derzeit in der {environment}-Umgebung.",
|
||||||
@@ -1786,6 +1786,7 @@
|
|||||||
"setup_instructions": "Einrichtung",
|
"setup_instructions": "Einrichtung",
|
||||||
"setup_integrations": "Integrationen einrichten",
|
"setup_integrations": "Integrationen einrichten",
|
||||||
"share_results": "Ergebnisse teilen",
|
"share_results": "Ergebnisse teilen",
|
||||||
|
"share_survey": "Umfrage teilen",
|
||||||
"share_the_link": "Teile den Link",
|
"share_the_link": "Teile den Link",
|
||||||
"share_the_link_to_get_responses": "Teile den Link, um Antworten einzusammeln",
|
"share_the_link_to_get_responses": "Teile den Link, um Antworten einzusammeln",
|
||||||
"show_all_responses_that_match": "Zeige alle Antworten, die übereinstimmen",
|
"show_all_responses_that_match": "Zeige alle Antworten, die übereinstimmen",
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "You can now log in with your new password"
|
"text": "You can now log in with your new password"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "Reset password"
|
"reset_password": "Reset password",
|
||||||
|
"reset_password_description": "You will be logged out to reset your password."
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "Create an account",
|
"create_account": "Create an account",
|
||||||
@@ -191,7 +192,6 @@
|
|||||||
"e_commerce": "E-Commerce",
|
"e_commerce": "E-Commerce",
|
||||||
"edit": "Edit",
|
"edit": "Edit",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"embed": "Embed",
|
|
||||||
"enterprise_license": "Enterprise License",
|
"enterprise_license": "Enterprise License",
|
||||||
"environment_not_found": "Environment not found",
|
"environment_not_found": "Environment not found",
|
||||||
"environment_notice": "You're currently in the {environment} environment.",
|
"environment_notice": "You're currently in the {environment} environment.",
|
||||||
@@ -1786,6 +1786,7 @@
|
|||||||
"setup_instructions": "Setup instructions",
|
"setup_instructions": "Setup instructions",
|
||||||
"setup_integrations": "Setup integrations",
|
"setup_integrations": "Setup integrations",
|
||||||
"share_results": "Share results",
|
"share_results": "Share results",
|
||||||
|
"share_survey": "Share survey",
|
||||||
"share_the_link": "Share the link",
|
"share_the_link": "Share the link",
|
||||||
"share_the_link_to_get_responses": "Share the link to get responses",
|
"share_the_link_to_get_responses": "Share the link to get responses",
|
||||||
"show_all_responses_that_match": "Show all responses that match",
|
"show_all_responses_that_match": "Show all responses that match",
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "Vous pouvez maintenant vous connecter avec votre nouveau mot de passe."
|
"text": "Vous pouvez maintenant vous connecter avec votre nouveau mot de passe."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "Réinitialiser le mot de passe"
|
"reset_password": "Réinitialiser le mot de passe",
|
||||||
|
"reset_password_description": "Vous serez déconnecté pour réinitialiser votre mot de passe."
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "Créer un compte",
|
"create_account": "Créer un compte",
|
||||||
@@ -191,7 +192,6 @@
|
|||||||
"e_commerce": "E-commerce",
|
"e_commerce": "E-commerce",
|
||||||
"edit": "Modifier",
|
"edit": "Modifier",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"embed": "Intégrer",
|
|
||||||
"enterprise_license": "Licence d'entreprise",
|
"enterprise_license": "Licence d'entreprise",
|
||||||
"environment_not_found": "Environnement non trouvé",
|
"environment_not_found": "Environnement non trouvé",
|
||||||
"environment_notice": "Vous êtes actuellement dans l'environnement {environment}.",
|
"environment_notice": "Vous êtes actuellement dans l'environnement {environment}.",
|
||||||
@@ -1786,6 +1786,7 @@
|
|||||||
"setup_instructions": "Instructions d'installation",
|
"setup_instructions": "Instructions d'installation",
|
||||||
"setup_integrations": "Configurer les intégrations",
|
"setup_integrations": "Configurer les intégrations",
|
||||||
"share_results": "Partager les résultats",
|
"share_results": "Partager les résultats",
|
||||||
|
"share_survey": "Partager l'enquête",
|
||||||
"share_the_link": "Partager le lien",
|
"share_the_link": "Partager le lien",
|
||||||
"share_the_link_to_get_responses": "Partagez le lien pour obtenir des réponses",
|
"share_the_link_to_get_responses": "Partagez le lien pour obtenir des réponses",
|
||||||
"show_all_responses_that_match": "Afficher toutes les réponses correspondantes",
|
"show_all_responses_that_match": "Afficher toutes les réponses correspondantes",
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "Agora você pode fazer login com sua nova senha"
|
"text": "Agora você pode fazer login com sua nova senha"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "Redefinir senha"
|
"reset_password": "Redefinir senha",
|
||||||
|
"reset_password_description": "Você será desconectado para redefinir sua senha."
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "Cria uma conta",
|
"create_account": "Cria uma conta",
|
||||||
@@ -191,7 +192,6 @@
|
|||||||
"e_commerce": "comércio eletrônico",
|
"e_commerce": "comércio eletrônico",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"embed": "incorporar",
|
|
||||||
"enterprise_license": "Licença Empresarial",
|
"enterprise_license": "Licença Empresarial",
|
||||||
"environment_not_found": "Ambiente não encontrado",
|
"environment_not_found": "Ambiente não encontrado",
|
||||||
"environment_notice": "Você está atualmente no ambiente {environment}.",
|
"environment_notice": "Você está atualmente no ambiente {environment}.",
|
||||||
@@ -1786,6 +1786,7 @@
|
|||||||
"setup_instructions": "Instruções de configuração",
|
"setup_instructions": "Instruções de configuração",
|
||||||
"setup_integrations": "Configurar integrações",
|
"setup_integrations": "Configurar integrações",
|
||||||
"share_results": "Compartilhar resultados",
|
"share_results": "Compartilhar resultados",
|
||||||
|
"share_survey": "Compartilhar pesquisa",
|
||||||
"share_the_link": "Compartilha o link",
|
"share_the_link": "Compartilha o link",
|
||||||
"share_the_link_to_get_responses": "Compartilha o link pra receber respostas",
|
"share_the_link_to_get_responses": "Compartilha o link pra receber respostas",
|
||||||
"show_all_responses_that_match": "Mostrar todas as respostas que correspondem",
|
"show_all_responses_that_match": "Mostrar todas as respostas que correspondem",
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "Pode agora iniciar sessão com a sua nova palavra-passe"
|
"text": "Pode agora iniciar sessão com a sua nova palavra-passe"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "Redefinir palavra-passe"
|
"reset_password": "Redefinir palavra-passe",
|
||||||
|
"reset_password_description": "Será desconectado para redefinir a sua palavra-passe."
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "Criar uma conta",
|
"create_account": "Criar uma conta",
|
||||||
@@ -191,7 +192,6 @@
|
|||||||
"e_commerce": "Comércio Eletrónico",
|
"e_commerce": "Comércio Eletrónico",
|
||||||
"edit": "Editar",
|
"edit": "Editar",
|
||||||
"email": "Email",
|
"email": "Email",
|
||||||
"embed": "Incorporar",
|
|
||||||
"enterprise_license": "Licença Enterprise",
|
"enterprise_license": "Licença Enterprise",
|
||||||
"environment_not_found": "Ambiente não encontrado",
|
"environment_not_found": "Ambiente não encontrado",
|
||||||
"environment_notice": "Está atualmente no ambiente {environment}.",
|
"environment_notice": "Está atualmente no ambiente {environment}.",
|
||||||
@@ -1786,6 +1786,7 @@
|
|||||||
"setup_instructions": "Instruções de configuração",
|
"setup_instructions": "Instruções de configuração",
|
||||||
"setup_integrations": "Configurar integrações",
|
"setup_integrations": "Configurar integrações",
|
||||||
"share_results": "Partilhar resultados",
|
"share_results": "Partilhar resultados",
|
||||||
|
"share_survey": "Partilhar inquérito",
|
||||||
"share_the_link": "Partilhar o link",
|
"share_the_link": "Partilhar o link",
|
||||||
"share_the_link_to_get_responses": "Partilhe o link para obter respostas",
|
"share_the_link_to_get_responses": "Partilhe o link para obter respostas",
|
||||||
"show_all_responses_that_match": "Mostrar todas as respostas que correspondem",
|
"show_all_responses_that_match": "Mostrar todas as respostas que correspondem",
|
||||||
|
|||||||
@@ -34,7 +34,8 @@
|
|||||||
"text": "您現在可以使用新密碼登入"
|
"text": "您現在可以使用新密碼登入"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"reset_password": "重設密碼"
|
"reset_password": "重設密碼",
|
||||||
|
"reset_password_description": "您將被登出以重設您的密碼。"
|
||||||
},
|
},
|
||||||
"invite": {
|
"invite": {
|
||||||
"create_account": "建立帳戶",
|
"create_account": "建立帳戶",
|
||||||
@@ -191,7 +192,6 @@
|
|||||||
"e_commerce": "電子商務",
|
"e_commerce": "電子商務",
|
||||||
"edit": "編輯",
|
"edit": "編輯",
|
||||||
"email": "電子郵件",
|
"email": "電子郵件",
|
||||||
"embed": "嵌入",
|
|
||||||
"enterprise_license": "企業授權",
|
"enterprise_license": "企業授權",
|
||||||
"environment_not_found": "找不到環境",
|
"environment_not_found": "找不到環境",
|
||||||
"environment_notice": "您目前在 '{'environment'}' 環境中。",
|
"environment_notice": "您目前在 '{'environment'}' 環境中。",
|
||||||
@@ -1786,6 +1786,7 @@
|
|||||||
"setup_instructions": "設定說明",
|
"setup_instructions": "設定說明",
|
||||||
"setup_integrations": "設定整合",
|
"setup_integrations": "設定整合",
|
||||||
"share_results": "分享結果",
|
"share_results": "分享結果",
|
||||||
|
"share_survey": "分享問卷",
|
||||||
"share_the_link": "分享連結",
|
"share_the_link": "分享連結",
|
||||||
"share_the_link_to_get_responses": "分享連結以取得回應",
|
"share_the_link_to_get_responses": "分享連結以取得回應",
|
||||||
"show_all_responses_that_match": "顯示所有相符的回應",
|
"show_all_responses_that_match": "顯示所有相符的回應",
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
import { cleanup, fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
import { afterEach, describe, expect, test, vi } from "vitest";
|
||||||
import { TOrganization } from "@formbricks/types/organizations";
|
import { TOrganization } from "@formbricks/types/organizations";
|
||||||
@@ -100,8 +99,6 @@ describe("DeleteAccountModal", () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
const removeItemSpy = vi.spyOn(window.localStorage, "removeItem");
|
|
||||||
|
|
||||||
const input = screen.getByTestId("deleteAccountConfirmation");
|
const input = screen.getByTestId("deleteAccountConfirmation");
|
||||||
fireEvent.change(input, { target: { value: mockUser.email } });
|
fireEvent.change(input, { target: { value: mockUser.email } });
|
||||||
|
|
||||||
@@ -113,8 +110,8 @@ describe("DeleteAccountModal", () => {
|
|||||||
expect(mockSignOut).toHaveBeenCalledWith({
|
expect(mockSignOut).toHaveBeenCalledWith({
|
||||||
reason: "account_deletion",
|
reason: "account_deletion",
|
||||||
redirect: false, // Updated to match new implementation
|
redirect: false, // Updated to match new implementation
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
expect(removeItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS);
|
|
||||||
expect(window.location.replace).toHaveBeenCalledWith("/auth/login");
|
expect(window.location.replace).toHaveBeenCalledWith("/auth/login");
|
||||||
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
expect(mockSetOpen).toHaveBeenCalledWith(false);
|
||||||
});
|
});
|
||||||
@@ -151,15 +148,13 @@ describe("DeleteAccountModal", () => {
|
|||||||
const form = screen.getByTestId("deleteAccountForm");
|
const form = screen.getByTestId("deleteAccountForm");
|
||||||
fireEvent.submit(form);
|
fireEvent.submit(form);
|
||||||
|
|
||||||
const removeItemSpy = vi.spyOn(window.localStorage, "removeItem");
|
|
||||||
|
|
||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(deleteUserAction).toHaveBeenCalled();
|
expect(deleteUserAction).toHaveBeenCalled();
|
||||||
expect(mockSignOut).toHaveBeenCalledWith({
|
expect(mockSignOut).toHaveBeenCalledWith({
|
||||||
reason: "account_deletion",
|
reason: "account_deletion",
|
||||||
redirect: false, // Updated to match new implementation
|
redirect: false, // Updated to match new implementation
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
expect(removeItemSpy).toHaveBeenCalledWith(FORMBRICKS_ENVIRONMENT_ID_LS);
|
|
||||||
expect(window.location.replace).toHaveBeenCalledWith(
|
expect(window.location.replace).toHaveBeenCalledWith(
|
||||||
"https://app.formbricks.com/s/clri52y3z8f221225wjdhsoo2"
|
"https://app.formbricks.com/s/clri52y3z8f221225wjdhsoo2"
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||||
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
import { DeleteDialog } from "@/modules/ui/components/delete-dialog";
|
||||||
import { Input } from "@/modules/ui/components/input";
|
import { Input } from "@/modules/ui/components/input";
|
||||||
@@ -39,12 +38,11 @@ export const DeleteAccountModal = ({
|
|||||||
setDeleting(true);
|
setDeleting(true);
|
||||||
await deleteUserAction();
|
await deleteUserAction();
|
||||||
|
|
||||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
|
||||||
|
|
||||||
// Sign out with account deletion reason (no automatic redirect)
|
// Sign out with account deletion reason (no automatic redirect)
|
||||||
await signOutWithAudit({
|
await signOutWithAudit({
|
||||||
reason: "account_deletion",
|
reason: "account_deletion",
|
||||||
redirect: false, // Prevent NextAuth automatic redirect
|
redirect: false, // Prevent NextAuth automatic redirect
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Manual redirect after signOut completes
|
// Manual redirect after signOut completes
|
||||||
|
|||||||
@@ -33,10 +33,11 @@ export const validateOtherOptionLengthForMultipleChoice = ({
|
|||||||
surveyQuestions,
|
surveyQuestions,
|
||||||
responseLanguage,
|
responseLanguage,
|
||||||
}: {
|
}: {
|
||||||
responseData: TResponseData;
|
responseData?: TResponseData;
|
||||||
surveyQuestions: TSurveyQuestion[];
|
surveyQuestions: TSurveyQuestion[];
|
||||||
responseLanguage?: string;
|
responseLanguage?: string;
|
||||||
}): string | undefined => {
|
}): string | undefined => {
|
||||||
|
if (!responseData) return undefined;
|
||||||
for (const [questionId, answer] of Object.entries(responseData)) {
|
for (const [questionId, answer] of Object.entries(responseData)) {
|
||||||
const question = surveyQuestions.find((q) => q.id === questionId);
|
const question = surveyQuestions.find((q) => q.id === questionId);
|
||||||
if (!question) continue;
|
if (!question) continue;
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { TContactAttributeKeyUpdateSchema } from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys";
|
import { TContactAttributeKeyUpdateSchema } from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys";
|
||||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||||
import { ContactAttributeKey } from "@prisma/client";
|
import { ContactAttributeKey, Prisma } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
@@ -55,7 +54,7 @@ export const updateContactAttributeKey = async (
|
|||||||
|
|
||||||
return ok(updatedKey);
|
return ok(updatedKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (
|
if (
|
||||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
@@ -106,7 +105,7 @@ export const deleteContactAttributeKey = async (
|
|||||||
|
|
||||||
return ok(deletedKey);
|
return ok(deletedKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (
|
if (
|
||||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const getContactAttributeKeyEndpoint: ZodOpenApiOperationObject = {
|
|||||||
id: ZContactAttributeKeyIdSchema,
|
id: ZContactAttributeKeyIdSchema,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Management API > Contact Attribute Keys"],
|
tags: ["Management API - Contact Attribute Keys"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Contact attribute key retrieved successfully.",
|
description: "Contact attribute key retrieved successfully.",
|
||||||
@@ -33,7 +33,7 @@ export const updateContactAttributeKeyEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "updateContactAttributeKey",
|
operationId: "updateContactAttributeKey",
|
||||||
summary: "Update a contact attribute key",
|
summary: "Update a contact attribute key",
|
||||||
description: "Updates a contact attribute key in the database.",
|
description: "Updates a contact attribute key in the database.",
|
||||||
tags: ["Management API > Contact Attribute Keys"],
|
tags: ["Management API - Contact Attribute Keys"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
id: ZContactAttributeKeyIdSchema,
|
id: ZContactAttributeKeyIdSchema,
|
||||||
@@ -64,7 +64,7 @@ export const deleteContactAttributeKeyEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "deleteContactAttributeKey",
|
operationId: "deleteContactAttributeKey",
|
||||||
summary: "Delete a contact attribute key",
|
summary: "Delete a contact attribute key",
|
||||||
description: "Deletes a contact attribute key from the database.",
|
description: "Deletes a contact attribute key from the database.",
|
||||||
tags: ["Management API > Contact Attribute Keys"],
|
tags: ["Management API - Contact Attribute Keys"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
id: ZContactAttributeKeyIdSchema,
|
id: ZContactAttributeKeyIdSchema,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { TContactAttributeKeyUpdateSchema } from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys";
|
import { TContactAttributeKeyUpdateSchema } from "@/modules/api/v2/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys";
|
||||||
import { ContactAttributeKey } from "@prisma/client";
|
import { ContactAttributeKey, Prisma } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
@@ -44,12 +43,12 @@ const mockUpdateInput: TContactAttributeKeyUpdateSchema = {
|
|||||||
description: "User's verified email address",
|
description: "User's verified email address",
|
||||||
};
|
};
|
||||||
|
|
||||||
const prismaNotFoundError = new PrismaClientKnownRequestError("Mock error message", {
|
const prismaNotFoundError = new Prisma.PrismaClientKnownRequestError("Mock error message", {
|
||||||
code: PrismaErrorType.RelatedRecordDoesNotExist,
|
code: PrismaErrorType.RelatedRecordDoesNotExist,
|
||||||
clientVersion: "0.0.1",
|
clientVersion: "0.0.1",
|
||||||
});
|
});
|
||||||
|
|
||||||
const prismaUniqueConstraintError = new PrismaClientKnownRequestError("Mock error message", {
|
const prismaUniqueConstraintError = new Prisma.PrismaClientKnownRequestError("Mock error message", {
|
||||||
code: PrismaErrorType.UniqueConstraintViolation,
|
code: PrismaErrorType.UniqueConstraintViolation,
|
||||||
clientVersion: "0.0.1",
|
clientVersion: "0.0.1",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import {
|
|||||||
} from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys";
|
} from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys";
|
||||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||||
import { ContactAttributeKey, Prisma } from "@prisma/client";
|
import { ContactAttributeKey, Prisma } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
@@ -58,7 +57,7 @@ export const createContactAttributeKey = async (
|
|||||||
|
|
||||||
return ok(createdContactAttributeKey);
|
return ok(createdContactAttributeKey);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (
|
if (
|
||||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const getContactAttributeKeysEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "getContactAttributeKeys",
|
operationId: "getContactAttributeKeys",
|
||||||
summary: "Get contact attribute keys",
|
summary: "Get contact attribute keys",
|
||||||
description: "Gets contact attribute keys from the database.",
|
description: "Gets contact attribute keys from the database.",
|
||||||
tags: ["Management API > Contact Attribute Keys"],
|
tags: ["Management API - Contact Attribute Keys"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
query: ZGetContactAttributeKeysFilter.sourceType(),
|
query: ZGetContactAttributeKeysFilter.sourceType(),
|
||||||
},
|
},
|
||||||
@@ -36,7 +36,7 @@ export const createContactAttributeKeyEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "createContactAttributeKey",
|
operationId: "createContactAttributeKey",
|
||||||
summary: "Create a contact attribute key",
|
summary: "Create a contact attribute key",
|
||||||
description: "Creates a contact attribute key in the database.",
|
description: "Creates a contact attribute key in the database.",
|
||||||
tags: ["Management API > Contact Attribute Keys"],
|
tags: ["Management API - Contact Attribute Keys"],
|
||||||
requestBody: {
|
requestBody: {
|
||||||
required: true,
|
required: true,
|
||||||
description: "The contact attribute key to create",
|
description: "The contact attribute key to create",
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import {
|
|||||||
TContactAttributeKeyInput,
|
TContactAttributeKeyInput,
|
||||||
TGetContactAttributeKeysFilter,
|
TGetContactAttributeKeysFilter,
|
||||||
} from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys";
|
} from "@/modules/api/v2/management/contact-attribute-keys/types/contact-attribute-keys";
|
||||||
import { ContactAttributeKey } from "@prisma/client";
|
import { ContactAttributeKey, Prisma } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
@@ -106,7 +105,7 @@ describe("createContactAttributeKey", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns conflict error when key already exists", async () => {
|
test("returns conflict error when key already exists", async () => {
|
||||||
const errToThrow = new PrismaClientKnownRequestError("Mock error message", {
|
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", {
|
||||||
code: PrismaErrorType.UniqueConstraintViolation,
|
code: PrismaErrorType.UniqueConstraintViolation,
|
||||||
clientVersion: "0.0.1",
|
clientVersion: "0.0.1",
|
||||||
});
|
});
|
||||||
@@ -129,7 +128,7 @@ describe("createContactAttributeKey", () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
test("returns not found error when related record does not exist", async () => {
|
test("returns not found error when related record does not exist", async () => {
|
||||||
const errToThrow = new PrismaClientKnownRequestError("Mock error message", {
|
const errToThrow = new Prisma.PrismaClientKnownRequestError("Mock error message", {
|
||||||
code: PrismaErrorType.RelatedRecordDoesNotExist,
|
code: PrismaErrorType.RelatedRecordDoesNotExist,
|
||||||
clientVersion: "0.0.1",
|
clientVersion: "0.0.1",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const getContactAttributeEndpoint: ZodOpenApiOperationObject = {
|
|||||||
contactAttributeId: z.string().cuid2(),
|
contactAttributeId: z.string().cuid2(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Management API > Contact Attributes"],
|
tags: ["Management API - Contact Attributes"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Contact retrieved successfully.",
|
description: "Contact retrieved successfully.",
|
||||||
@@ -29,7 +29,7 @@ export const deleteContactAttributeEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "deleteContactAttribute",
|
operationId: "deleteContactAttribute",
|
||||||
summary: "Delete a contact attribute",
|
summary: "Delete a contact attribute",
|
||||||
description: "Deletes a contact attribute from the database.",
|
description: "Deletes a contact attribute from the database.",
|
||||||
tags: ["Management API > Contact Attributes"],
|
tags: ["Management API - Contact Attributes"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
contactAttributeId: z.string().cuid2(),
|
contactAttributeId: z.string().cuid2(),
|
||||||
@@ -51,7 +51,7 @@ export const updateContactAttributeEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "updateContactAttribute",
|
operationId: "updateContactAttribute",
|
||||||
summary: "Update a contact attribute",
|
summary: "Update a contact attribute",
|
||||||
description: "Updates a contact attribute in the database.",
|
description: "Updates a contact attribute in the database.",
|
||||||
tags: ["Management API > Contact Attributes"],
|
tags: ["Management API - Contact Attributes"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
contactAttributeId: z.string().cuid2(),
|
contactAttributeId: z.string().cuid2(),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const getContactAttributesEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "getContactAttributes",
|
operationId: "getContactAttributes",
|
||||||
summary: "Get contact attributes",
|
summary: "Get contact attributes",
|
||||||
description: "Gets contact attributes from the database.",
|
description: "Gets contact attributes from the database.",
|
||||||
tags: ["Management API > Contact Attributes"],
|
tags: ["Management API - Contact Attributes"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
query: ZGetContactAttributesFilter,
|
query: ZGetContactAttributesFilter,
|
||||||
},
|
},
|
||||||
@@ -36,7 +36,7 @@ export const createContactAttributeEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "createContactAttribute",
|
operationId: "createContactAttribute",
|
||||||
summary: "Create a contact attribute",
|
summary: "Create a contact attribute",
|
||||||
description: "Creates a contact attribute in the database.",
|
description: "Creates a contact attribute in the database.",
|
||||||
tags: ["Management API > Contact Attributes"],
|
tags: ["Management API - Contact Attributes"],
|
||||||
requestBody: {
|
requestBody: {
|
||||||
required: true,
|
required: true,
|
||||||
description: "The contact attribute to create",
|
description: "The contact attribute to create",
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export const getContactEndpoint: ZodOpenApiOperationObject = {
|
|||||||
contactId: z.string().cuid2(),
|
contactId: z.string().cuid2(),
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Management API > Contacts"],
|
tags: ["Management API - Contacts"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Contact retrieved successfully.",
|
description: "Contact retrieved successfully.",
|
||||||
@@ -29,7 +29,7 @@ export const deleteContactEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "deleteContact",
|
operationId: "deleteContact",
|
||||||
summary: "Delete a contact",
|
summary: "Delete a contact",
|
||||||
description: "Deletes a contact from the database.",
|
description: "Deletes a contact from the database.",
|
||||||
tags: ["Management API > Contacts"],
|
tags: ["Management API - Contacts"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
contactId: z.string().cuid2(),
|
contactId: z.string().cuid2(),
|
||||||
@@ -51,7 +51,7 @@ export const updateContactEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "updateContact",
|
operationId: "updateContact",
|
||||||
summary: "Update a contact",
|
summary: "Update a contact",
|
||||||
description: "Updates a contact in the database.",
|
description: "Updates a contact in the database.",
|
||||||
tags: ["Management API > Contacts"],
|
tags: ["Management API - Contacts"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
contactId: z.string().cuid2(),
|
contactId: z.string().cuid2(),
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const getContactsEndpoint: ZodOpenApiOperationObject = {
|
|||||||
requestParams: {
|
requestParams: {
|
||||||
query: ZGetContactsFilter,
|
query: ZGetContactsFilter,
|
||||||
},
|
},
|
||||||
tags: ["Management API > Contacts"],
|
tags: ["Management API - Contacts"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Contacts retrieved successfully.",
|
description: "Contacts retrieved successfully.",
|
||||||
@@ -33,7 +33,7 @@ export const createContactEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "createContact",
|
operationId: "createContact",
|
||||||
summary: "Create a contact",
|
summary: "Create a contact",
|
||||||
description: "Creates a contact in the database.",
|
description: "Creates a contact in the database.",
|
||||||
tags: ["Management API > Contacts"],
|
tags: ["Management API - Contacts"],
|
||||||
requestBody: {
|
requestBody: {
|
||||||
required: true,
|
required: true,
|
||||||
description: "The contact to create",
|
description: "The contact to create",
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
import { Prisma } from "@prisma/client";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
import { Result, err, ok } from "@formbricks/types/error-handlers";
|
||||||
@@ -19,7 +19,7 @@ export const deleteDisplay = async (displayId: string): Promise<Result<boolean,
|
|||||||
|
|
||||||
return ok(true);
|
return ok(true);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (
|
if (
|
||||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const getResponseEndpoint: ZodOpenApiOperationObject = {
|
|||||||
id: ZResponseIdSchema,
|
id: ZResponseIdSchema,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Management API > Responses"],
|
tags: ["Management API - Responses"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Response retrieved successfully.",
|
description: "Response retrieved successfully.",
|
||||||
@@ -31,7 +31,7 @@ export const deleteResponseEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "deleteResponse",
|
operationId: "deleteResponse",
|
||||||
summary: "Delete a response",
|
summary: "Delete a response",
|
||||||
description: "Deletes a response from the database.",
|
description: "Deletes a response from the database.",
|
||||||
tags: ["Management API > Responses"],
|
tags: ["Management API - Responses"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
id: ZResponseIdSchema,
|
id: ZResponseIdSchema,
|
||||||
@@ -53,7 +53,7 @@ export const updateResponseEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "updateResponse",
|
operationId: "updateResponse",
|
||||||
summary: "Update a response",
|
summary: "Update a response",
|
||||||
description: "Updates a response in the database.",
|
description: "Updates a response in the database.",
|
||||||
tags: ["Management API > Responses"],
|
tags: ["Management API - Responses"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
id: ZResponseIdSchema,
|
id: ZResponseIdSchema,
|
||||||
|
|||||||
@@ -3,8 +3,7 @@ import { getSurveyQuestions } from "@/modules/api/v2/management/responses/[respo
|
|||||||
import { findAndDeleteUploadedFilesInResponse } from "@/modules/api/v2/management/responses/[responseId]/lib/utils";
|
import { findAndDeleteUploadedFilesInResponse } from "@/modules/api/v2/management/responses/[responseId]/lib/utils";
|
||||||
import { ZResponseUpdateSchema } from "@/modules/api/v2/management/responses/[responseId]/types/responses";
|
import { ZResponseUpdateSchema } from "@/modules/api/v2/management/responses/[responseId]/types/responses";
|
||||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||||
import { Response } from "@prisma/client";
|
import { Prisma, Response } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
@@ -56,7 +55,7 @@ export const deleteResponse = async (responseId: string): Promise<Result<Respons
|
|||||||
|
|
||||||
return ok(deletedResponse);
|
return ok(deletedResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (
|
if (
|
||||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
@@ -89,7 +88,7 @@ export const updateResponse = async (
|
|||||||
|
|
||||||
return ok(updatedResponse);
|
return ok(updatedResponse);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (
|
if (
|
||||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { displayId, mockDisplay } from "./__mocks__/display.mock";
|
import { displayId, mockDisplay } from "./__mocks__/display.mock";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
import { Prisma } from "@prisma/client";
|
||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
@@ -39,7 +39,7 @@ describe("Display Lib", () => {
|
|||||||
|
|
||||||
test("return a not_found error when the display is not found", async () => {
|
test("return a not_found error when the display is not found", async () => {
|
||||||
vi.mocked(prisma.display.delete).mockRejectedValue(
|
vi.mocked(prisma.display.delete).mockRejectedValue(
|
||||||
new PrismaClientKnownRequestError("Display not found", {
|
new Prisma.PrismaClientKnownRequestError("Display not found", {
|
||||||
code: PrismaErrorType.RelatedRecordDoesNotExist,
|
code: PrismaErrorType.RelatedRecordDoesNotExist,
|
||||||
clientVersion: "1.0.0",
|
clientVersion: "1.0.0",
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { response, responseId, responseInput, survey } from "./__mocks__/response.mock";
|
import { response, responseId, responseInput, survey } from "./__mocks__/response.mock";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
import { Prisma } from "@prisma/client";
|
||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
@@ -154,7 +154,7 @@ describe("Response Lib", () => {
|
|||||||
|
|
||||||
test("handle prisma client error code P2025", async () => {
|
test("handle prisma client error code P2025", async () => {
|
||||||
vi.mocked(prisma.response.delete).mockRejectedValue(
|
vi.mocked(prisma.response.delete).mockRejectedValue(
|
||||||
new PrismaClientKnownRequestError("Response not found", {
|
new Prisma.PrismaClientKnownRequestError("Response not found", {
|
||||||
code: PrismaErrorType.RelatedRecordDoesNotExist,
|
code: PrismaErrorType.RelatedRecordDoesNotExist,
|
||||||
clientVersion: "1.0.0",
|
clientVersion: "1.0.0",
|
||||||
meta: {
|
meta: {
|
||||||
@@ -208,7 +208,7 @@ describe("Response Lib", () => {
|
|||||||
|
|
||||||
test("return a not_found error when the response is not found", async () => {
|
test("return a not_found error when the response is not found", async () => {
|
||||||
vi.mocked(prisma.response.update).mockRejectedValue(
|
vi.mocked(prisma.response.update).mockRejectedValue(
|
||||||
new PrismaClientKnownRequestError("Response not found", {
|
new Prisma.PrismaClientKnownRequestError("Response not found", {
|
||||||
code: PrismaErrorType.RelatedRecordDoesNotExist,
|
code: PrismaErrorType.RelatedRecordDoesNotExist,
|
||||||
clientVersion: "1.0.0",
|
clientVersion: "1.0.0",
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const getResponsesEndpoint: ZodOpenApiOperationObject = {
|
|||||||
requestParams: {
|
requestParams: {
|
||||||
query: ZGetResponsesFilter.sourceType(),
|
query: ZGetResponsesFilter.sourceType(),
|
||||||
},
|
},
|
||||||
tags: ["Management API > Responses"],
|
tags: ["Management API - Responses"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Responses retrieved successfully.",
|
description: "Responses retrieved successfully.",
|
||||||
@@ -33,7 +33,7 @@ export const createResponseEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "createResponse",
|
operationId: "createResponse",
|
||||||
summary: "Create a response",
|
summary: "Create a response",
|
||||||
description: "Creates a response in the database.",
|
description: "Creates a response in the database.",
|
||||||
tags: ["Management API > Responses"],
|
tags: ["Management API - Responses"],
|
||||||
requestBody: {
|
requestBody: {
|
||||||
required: true,
|
required: true,
|
||||||
description: "The response to create",
|
description: "The response to create",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const getPersonalizedSurveyLink: ZodOpenApiOperationObject = {
|
|||||||
requestParams: {
|
requestParams: {
|
||||||
path: ZContactLinkParams,
|
path: ZContactLinkParams,
|
||||||
},
|
},
|
||||||
tags: ["Management API > Surveys > Contact Links"],
|
tags: ["Management API - Surveys - Contact Links"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Personalized survey link retrieved successfully.",
|
description: "Personalized survey link retrieved successfully.",
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export const getContactLinksBySegmentEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "getContactLinksBySegment",
|
operationId: "getContactLinksBySegment",
|
||||||
summary: "Get survey links for contacts in a segment",
|
summary: "Get survey links for contacts in a segment",
|
||||||
description: "Generates personalized survey links for contacts in a segment.",
|
description: "Generates personalized survey links for contacts in a segment.",
|
||||||
tags: ["Management API > Surveys > Contact Links"],
|
tags: ["Management API - Surveys - Contact Links"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: ZContactLinksBySegmentParams,
|
path: ZContactLinksBySegmentParams,
|
||||||
query: ZContactLinksBySegmentQuery,
|
query: ZContactLinksBySegmentQuery,
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export const getSurveyEndpoint: ZodOpenApiOperationObject = {
|
|||||||
id: surveyIdSchema,
|
id: surveyIdSchema,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Management API > Surveys"],
|
tags: ["Management API - Surveys"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Response retrieved successfully.",
|
description: "Response retrieved successfully.",
|
||||||
@@ -30,7 +30,7 @@ export const deleteSurveyEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "deleteSurvey",
|
operationId: "deleteSurvey",
|
||||||
summary: "Delete a survey",
|
summary: "Delete a survey",
|
||||||
description: "Deletes a survey from the database.",
|
description: "Deletes a survey from the database.",
|
||||||
tags: ["Management API > Surveys"],
|
tags: ["Management API - Surveys"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
id: surveyIdSchema,
|
id: surveyIdSchema,
|
||||||
@@ -52,7 +52,7 @@ export const updateSurveyEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "updateSurvey",
|
operationId: "updateSurvey",
|
||||||
summary: "Update a survey",
|
summary: "Update a survey",
|
||||||
description: "Updates a survey in the database.",
|
description: "Updates a survey in the database.",
|
||||||
tags: ["Management API > Surveys"],
|
tags: ["Management API - Surveys"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
id: surveyIdSchema,
|
id: surveyIdSchema,
|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
// import {
|
|
||||||
// deleteSurveyEndpoint,
|
|
||||||
// getSurveyEndpoint,
|
|
||||||
// updateSurveyEndpoint,
|
|
||||||
// } from "@/modules/api/v2/management/surveys/[surveyId]/lib/openapi";
|
|
||||||
import { managementServer } from "@/modules/api/v2/management/lib/openapi";
|
import { managementServer } from "@/modules/api/v2/management/lib/openapi";
|
||||||
import { getPersonalizedSurveyLink } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/openapi";
|
import { getPersonalizedSurveyLink } from "@/modules/api/v2/management/surveys/[surveyId]/contact-links/contacts/[contactId]/lib/openapi";
|
||||||
import { ZGetSurveysFilter, ZSurveyInput } from "@/modules/api/v2/management/surveys/types/surveys";
|
import { ZGetSurveysFilter, ZSurveyInput } from "@/modules/api/v2/management/surveys/types/surveys";
|
||||||
@@ -17,7 +12,7 @@ export const getSurveysEndpoint: ZodOpenApiOperationObject = {
|
|||||||
requestParams: {
|
requestParams: {
|
||||||
query: ZGetSurveysFilter,
|
query: ZGetSurveysFilter,
|
||||||
},
|
},
|
||||||
tags: ["Management API > Surveys"],
|
tags: ["Management API - Surveys"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Surveys retrieved successfully.",
|
description: "Surveys retrieved successfully.",
|
||||||
@@ -34,7 +29,7 @@ export const createSurveyEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "createSurvey",
|
operationId: "createSurvey",
|
||||||
summary: "Create a survey",
|
summary: "Create a survey",
|
||||||
description: "Creates a survey in the database.",
|
description: "Creates a survey in the database.",
|
||||||
tags: ["Management API > Surveys"],
|
tags: ["Management API - Surveys"],
|
||||||
requestBody: {
|
requestBody: {
|
||||||
required: true,
|
required: true,
|
||||||
description: "The survey to create",
|
description: "The survey to create",
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ export const getWebhookEndpoint: ZodOpenApiOperationObject = {
|
|||||||
id: ZWebhookIdSchema,
|
id: ZWebhookIdSchema,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Management API > Webhooks"],
|
tags: ["Management API - Webhooks"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Webhook retrieved successfully.",
|
description: "Webhook retrieved successfully.",
|
||||||
@@ -31,7 +31,7 @@ export const deleteWebhookEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "deleteWebhook",
|
operationId: "deleteWebhook",
|
||||||
summary: "Delete a webhook",
|
summary: "Delete a webhook",
|
||||||
description: "Deletes a webhook from the database.",
|
description: "Deletes a webhook from the database.",
|
||||||
tags: ["Management API > Webhooks"],
|
tags: ["Management API - Webhooks"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
id: ZWebhookIdSchema,
|
id: ZWebhookIdSchema,
|
||||||
@@ -53,7 +53,7 @@ export const updateWebhookEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "updateWebhook",
|
operationId: "updateWebhook",
|
||||||
summary: "Update a webhook",
|
summary: "Update a webhook",
|
||||||
description: "Updates a webhook in the database.",
|
description: "Updates a webhook in the database.",
|
||||||
tags: ["Management API > Webhooks"],
|
tags: ["Management API - Webhooks"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
id: ZWebhookIdSchema,
|
id: ZWebhookIdSchema,
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { WebhookSource } from "@prisma/client";
|
import { Prisma, WebhookSource } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
|
|
||||||
export const mockedPrismaWebhookUpdateReturn = {
|
export const mockedPrismaWebhookUpdateReturn = {
|
||||||
@@ -14,7 +13,7 @@ export const mockedPrismaWebhookUpdateReturn = {
|
|||||||
surveyIds: [],
|
surveyIds: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
export const prismaNotFoundError = new PrismaClientKnownRequestError("Record does not exist", {
|
export const prismaNotFoundError = new Prisma.PrismaClientKnownRequestError("Record does not exist", {
|
||||||
code: PrismaErrorType.RecordDoesNotExist,
|
code: PrismaErrorType.RecordDoesNotExist,
|
||||||
clientVersion: "PrismaClient 4.0.0",
|
clientVersion: "PrismaClient 4.0.0",
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ZWebhookUpdateSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks";
|
import { ZWebhookUpdateSchema } from "@/modules/api/v2/management/webhooks/[webhookId]/types/webhooks";
|
||||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||||
import { Webhook } from "@prisma/client";
|
import { Prisma, Webhook } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
@@ -45,7 +44,7 @@ export const updateWebhook = async (
|
|||||||
|
|
||||||
return ok(updatedWebhook);
|
return ok(updatedWebhook);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (
|
if (
|
||||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
@@ -73,7 +72,7 @@ export const deleteWebhook = async (webhookId: string): Promise<Result<Webhook,
|
|||||||
|
|
||||||
return ok(deletedWebhook);
|
return ok(deletedWebhook);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (
|
if (
|
||||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const getWebhooksEndpoint: ZodOpenApiOperationObject = {
|
|||||||
requestParams: {
|
requestParams: {
|
||||||
query: ZGetWebhooksFilter.sourceType(),
|
query: ZGetWebhooksFilter.sourceType(),
|
||||||
},
|
},
|
||||||
tags: ["Management API > Webhooks"],
|
tags: ["Management API - Webhooks"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Webhooks retrieved successfully.",
|
description: "Webhooks retrieved successfully.",
|
||||||
@@ -33,7 +33,7 @@ export const createWebhookEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "createWebhook",
|
operationId: "createWebhook",
|
||||||
summary: "Create a webhook",
|
summary: "Create a webhook",
|
||||||
description: "Creates a webhook in the database.",
|
description: "Creates a webhook in the database.",
|
||||||
tags: ["Management API > Webhooks"],
|
tags: ["Management API - Webhooks"],
|
||||||
requestBody: {
|
requestBody: {
|
||||||
required: true,
|
required: true,
|
||||||
description: "The webhook to create",
|
description: "The webhook to create",
|
||||||
|
|||||||
@@ -66,43 +66,43 @@ const document = createDocument({
|
|||||||
description: "Operations for managing your API key.",
|
description: "Operations for managing your API key.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Management API > Responses",
|
name: "Management API - Responses",
|
||||||
description: "Operations for managing responses.",
|
description: "Operations for managing responses.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Management API > Contacts",
|
name: "Management API - Contacts",
|
||||||
description: "Operations for managing contacts.",
|
description: "Operations for managing contacts.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Management API > Contact Attributes",
|
name: "Management API - Contact Attributes",
|
||||||
description: "Operations for managing contact attributes.",
|
description: "Operations for managing contact attributes.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Management API > Contact Attributes Keys",
|
name: "Management API - Contact Attribute Keys",
|
||||||
description: "Operations for managing contact attributes keys.",
|
description: "Operations for managing contact attribute keys.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Management API > Surveys",
|
name: "Management API - Surveys",
|
||||||
description: "Operations for managing surveys.",
|
description: "Operations for managing surveys.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Management API > Surveys > Contact Links",
|
name: "Management API - Surveys - Contact Links",
|
||||||
description: "Operations for generating personalized survey links for contacts.",
|
description: "Operations for generating personalized survey links for contacts.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Management API > Webhooks",
|
name: "Management API - Webhooks",
|
||||||
description: "Operations for managing webhooks.",
|
description: "Operations for managing webhooks.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Organizations API > Teams",
|
name: "Organizations API - Teams",
|
||||||
description: "Operations for managing teams.",
|
description: "Operations for managing teams.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Organizations API > Project Teams",
|
name: "Organizations API - Project Teams",
|
||||||
description: "Operations for managing project teams.",
|
description: "Operations for managing project teams.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Organizations API > Users",
|
name: "Organizations API - Users",
|
||||||
description: "Operations for managing users.",
|
description: "Operations for managing users.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const getProjectTeamsEndpoint: ZodOpenApiOperationObject = {
|
|||||||
organizationId: ZOrganizationIdSchema,
|
organizationId: ZOrganizationIdSchema,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Organizations API > Project Teams"],
|
tags: ["Organizations API - Project Teams"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Project teams retrieved successfully.",
|
description: "Project teams retrieved successfully.",
|
||||||
@@ -42,7 +42,7 @@ export const createProjectTeamEndpoint: ZodOpenApiOperationObject = {
|
|||||||
organizationId: ZOrganizationIdSchema,
|
organizationId: ZOrganizationIdSchema,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Organizations API > Project Teams"],
|
tags: ["Organizations API - Project Teams"],
|
||||||
requestBody: {
|
requestBody: {
|
||||||
required: true,
|
required: true,
|
||||||
description: "The project team to create",
|
description: "The project team to create",
|
||||||
@@ -68,7 +68,7 @@ export const deleteProjectTeamEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "deleteProjectTeam",
|
operationId: "deleteProjectTeam",
|
||||||
summary: "Delete a project team",
|
summary: "Delete a project team",
|
||||||
description: "Deletes a project team from the database.",
|
description: "Deletes a project team from the database.",
|
||||||
tags: ["Organizations API > Project Teams"],
|
tags: ["Organizations API - Project Teams"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
query: ZGetProjectTeamUpdateFilter.required(),
|
query: ZGetProjectTeamUpdateFilter.required(),
|
||||||
path: z.object({
|
path: z.object({
|
||||||
@@ -91,7 +91,7 @@ export const updateProjectTeamEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "updateProjectTeam",
|
operationId: "updateProjectTeam",
|
||||||
summary: "Update a project team",
|
summary: "Update a project team",
|
||||||
description: "Updates a project team in the database.",
|
description: "Updates a project team in the database.",
|
||||||
tags: ["Organizations API > Project Teams"],
|
tags: ["Organizations API - Project Teams"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
organizationId: ZOrganizationIdSchema,
|
organizationId: ZOrganizationIdSchema,
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ export const getTeamEndpoint: ZodOpenApiOperationObject = {
|
|||||||
organizationId: ZOrganizationIdSchema,
|
organizationId: ZOrganizationIdSchema,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Organizations API > Teams"],
|
tags: ["Organizations API - Teams"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Team retrieved successfully.",
|
description: "Team retrieved successfully.",
|
||||||
@@ -33,7 +33,7 @@ export const deleteTeamEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "deleteTeam",
|
operationId: "deleteTeam",
|
||||||
summary: "Delete a team",
|
summary: "Delete a team",
|
||||||
description: "Deletes a team from the database.",
|
description: "Deletes a team from the database.",
|
||||||
tags: ["Organizations API > Teams"],
|
tags: ["Organizations API - Teams"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
id: ZTeamIdSchema,
|
id: ZTeamIdSchema,
|
||||||
@@ -56,7 +56,7 @@ export const updateTeamEndpoint: ZodOpenApiOperationObject = {
|
|||||||
operationId: "updateTeam",
|
operationId: "updateTeam",
|
||||||
summary: "Update a team",
|
summary: "Update a team",
|
||||||
description: "Updates a team in the database.",
|
description: "Updates a team in the database.",
|
||||||
tags: ["Organizations API > Teams"],
|
tags: ["Organizations API - Teams"],
|
||||||
requestParams: {
|
requestParams: {
|
||||||
path: z.object({
|
path: z.object({
|
||||||
id: ZTeamIdSchema,
|
id: ZTeamIdSchema,
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { ZTeamUpdateSchema } from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams";
|
import { ZTeamUpdateSchema } from "@/modules/api/v2/organizations/[organizationId]/teams/[teamId]/types/teams";
|
||||||
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||||
import { Team } from "@prisma/client";
|
import { Prisma, Team } from "@prisma/client";
|
||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
@@ -51,7 +50,7 @@ export const deleteTeam = async (
|
|||||||
|
|
||||||
return ok(deletedTeam);
|
return ok(deletedTeam);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (
|
if (
|
||||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
@@ -89,7 +88,7 @@ export const updateTeam = async (
|
|||||||
|
|
||||||
return ok(updatedTeam);
|
return ok(updatedTeam);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
if (
|
if (
|
||||||
error.code === PrismaErrorType.RecordDoesNotExist ||
|
error.code === PrismaErrorType.RecordDoesNotExist ||
|
||||||
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
error.code === PrismaErrorType.RelatedRecordDoesNotExist
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { PrismaClientKnownRequestError } from "@prisma/client/runtime/library";
|
import { Prisma } from "@prisma/client";
|
||||||
import { describe, expect, test, vi } from "vitest";
|
import { describe, expect, test, vi } from "vitest";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { PrismaErrorType } from "@formbricks/database/types/error";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
@@ -74,7 +74,7 @@ describe("Teams Lib", () => {
|
|||||||
|
|
||||||
test("returns not_found error on known prisma error", async () => {
|
test("returns not_found error on known prisma error", async () => {
|
||||||
(prisma.team.delete as any).mockRejectedValueOnce(
|
(prisma.team.delete as any).mockRejectedValueOnce(
|
||||||
new PrismaClientKnownRequestError("Not found", {
|
new Prisma.PrismaClientKnownRequestError("Not found", {
|
||||||
code: PrismaErrorType.RecordDoesNotExist,
|
code: PrismaErrorType.RecordDoesNotExist,
|
||||||
clientVersion: "1.0.0",
|
clientVersion: "1.0.0",
|
||||||
meta: {},
|
meta: {},
|
||||||
@@ -120,7 +120,7 @@ describe("Teams Lib", () => {
|
|||||||
|
|
||||||
test("returns not_found error when update fails due to missing team", async () => {
|
test("returns not_found error when update fails due to missing team", async () => {
|
||||||
(prisma.team.update as any).mockRejectedValueOnce(
|
(prisma.team.update as any).mockRejectedValueOnce(
|
||||||
new PrismaClientKnownRequestError("Not found", {
|
new Prisma.PrismaClientKnownRequestError("Not found", {
|
||||||
code: PrismaErrorType.RecordDoesNotExist,
|
code: PrismaErrorType.RecordDoesNotExist,
|
||||||
clientVersion: "1.0.0",
|
clientVersion: "1.0.0",
|
||||||
meta: {},
|
meta: {},
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export const getTeamsEndpoint: ZodOpenApiOperationObject = {
|
|||||||
}),
|
}),
|
||||||
query: ZGetTeamsFilter.sourceType(),
|
query: ZGetTeamsFilter.sourceType(),
|
||||||
},
|
},
|
||||||
tags: ["Organizations API > Teams"],
|
tags: ["Organizations API - Teams"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Teams retrieved successfully.",
|
description: "Teams retrieved successfully.",
|
||||||
@@ -46,7 +46,7 @@ export const createTeamEndpoint: ZodOpenApiOperationObject = {
|
|||||||
organizationId: ZOrganizationIdSchema,
|
organizationId: ZOrganizationIdSchema,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Organizations API > Teams"],
|
tags: ["Organizations API - Teams"],
|
||||||
requestBody: {
|
requestBody: {
|
||||||
required: true,
|
required: true,
|
||||||
description: "The team to create",
|
description: "The team to create",
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ export const getUsersEndpoint: ZodOpenApiOperationObject = {
|
|||||||
}),
|
}),
|
||||||
query: ZGetUsersFilter.sourceType(),
|
query: ZGetUsersFilter.sourceType(),
|
||||||
},
|
},
|
||||||
tags: ["Organizations API > Users"],
|
tags: ["Organizations API - Users"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Users retrieved successfully.",
|
description: "Users retrieved successfully.",
|
||||||
@@ -42,7 +42,7 @@ export const createUserEndpoint: ZodOpenApiOperationObject = {
|
|||||||
organizationId: ZOrganizationIdSchema,
|
organizationId: ZOrganizationIdSchema,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Organizations API > Users"],
|
tags: ["Organizations API - Users"],
|
||||||
requestBody: {
|
requestBody: {
|
||||||
required: true,
|
required: true,
|
||||||
description: "The user to create",
|
description: "The user to create",
|
||||||
@@ -73,7 +73,7 @@ export const updateUserEndpoint: ZodOpenApiOperationObject = {
|
|||||||
organizationId: ZOrganizationIdSchema,
|
organizationId: ZOrganizationIdSchema,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
tags: ["Organizations API > Users"],
|
tags: ["Organizations API - Users"],
|
||||||
requestBody: {
|
requestBody: {
|
||||||
required: true,
|
required: true,
|
||||||
description: "The user to update",
|
description: "The user to update",
|
||||||
|
|||||||
@@ -13,7 +13,13 @@ export const logSignOutAction = async (
|
|||||||
userId: string,
|
userId: string,
|
||||||
userEmail: string,
|
userEmail: string,
|
||||||
context: {
|
context: {
|
||||||
reason?: "user_initiated" | "account_deletion" | "email_change" | "session_timeout" | "forced_logout";
|
reason?:
|
||||||
|
| "user_initiated"
|
||||||
|
| "account_deletion"
|
||||||
|
| "email_change"
|
||||||
|
| "session_timeout"
|
||||||
|
| "forced_logout"
|
||||||
|
| "password_reset";
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
"use server";
|
"use server";
|
||||||
|
|
||||||
|
import { PASSWORD_RESET_DISABLED } from "@/lib/constants";
|
||||||
import { actionClient } from "@/lib/utils/action-client";
|
import { actionClient } from "@/lib/utils/action-client";
|
||||||
import { getUserByEmail } from "@/modules/auth/lib/user";
|
import { getUserByEmail } from "@/modules/auth/lib/user";
|
||||||
import { sendForgotPasswordEmail } from "@/modules/email";
|
import { sendForgotPasswordEmail } from "@/modules/email";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
import { OperationNotAllowedError } from "@formbricks/types/errors";
|
||||||
import { ZUserEmail } from "@formbricks/types/user";
|
import { ZUserEmail } from "@formbricks/types/user";
|
||||||
|
|
||||||
const ZForgotPasswordAction = z.object({
|
const ZForgotPasswordAction = z.object({
|
||||||
@@ -13,9 +15,15 @@ const ZForgotPasswordAction = z.object({
|
|||||||
export const forgotPasswordAction = actionClient
|
export const forgotPasswordAction = actionClient
|
||||||
.schema(ZForgotPasswordAction)
|
.schema(ZForgotPasswordAction)
|
||||||
.action(async ({ parsedInput }) => {
|
.action(async ({ parsedInput }) => {
|
||||||
|
if (PASSWORD_RESET_DISABLED) {
|
||||||
|
throw new OperationNotAllowedError("Password reset is disabled");
|
||||||
|
}
|
||||||
|
|
||||||
const user = await getUserByEmail(parsedInput.email);
|
const user = await getUserByEmail(parsedInput.email);
|
||||||
if (user) {
|
|
||||||
|
if (user && user.identityProvider === "email") {
|
||||||
await sendForgotPasswordEmail(user);
|
await sendForgotPasswordEmail(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
return { success: true };
|
return { success: true };
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,13 +1,21 @@
|
|||||||
|
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
||||||
import { logSignOutAction } from "@/modules/auth/actions/sign-out";
|
import { logSignOutAction } from "@/modules/auth/actions/sign-out";
|
||||||
import { signOut } from "next-auth/react";
|
import { signOut } from "next-auth/react";
|
||||||
import { logger } from "@formbricks/logger";
|
import { logger } from "@formbricks/logger";
|
||||||
|
|
||||||
interface UseSignOutOptions {
|
interface UseSignOutOptions {
|
||||||
reason?: "user_initiated" | "account_deletion" | "email_change" | "session_timeout" | "forced_logout";
|
reason?:
|
||||||
|
| "user_initiated"
|
||||||
|
| "account_deletion"
|
||||||
|
| "email_change"
|
||||||
|
| "session_timeout"
|
||||||
|
| "forced_logout"
|
||||||
|
| "password_reset";
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
redirect?: boolean;
|
redirect?: boolean;
|
||||||
callbackUrl?: string;
|
callbackUrl?: string;
|
||||||
|
clearEnvironmentId?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SessionUser {
|
interface SessionUser {
|
||||||
@@ -36,6 +44,10 @@ export const useSignOut = (sessionUser?: SessionUser | null) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (options?.clearEnvironmentId) {
|
||||||
|
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
||||||
|
}
|
||||||
|
|
||||||
// Call NextAuth signOut
|
// Call NextAuth signOut
|
||||||
return await signOut({
|
return await signOut({
|
||||||
redirect: options?.redirect,
|
redirect: options?.redirect,
|
||||||
|
|||||||
@@ -78,6 +78,7 @@ export const getUserByEmail = reactCache(async (email: string) => {
|
|||||||
email: true,
|
email: true,
|
||||||
emailVerified: true,
|
emailVerified: true,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
identityProvider: true,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -283,7 +283,13 @@ export const logSignOut = (
|
|||||||
userId: string,
|
userId: string,
|
||||||
userEmail: string,
|
userEmail: string,
|
||||||
context?: {
|
context?: {
|
||||||
reason?: "user_initiated" | "account_deletion" | "email_change" | "session_timeout" | "forced_logout";
|
reason?:
|
||||||
|
| "user_initiated"
|
||||||
|
| "account_deletion"
|
||||||
|
| "email_change"
|
||||||
|
| "session_timeout"
|
||||||
|
| "forced_logout"
|
||||||
|
| "password_reset";
|
||||||
redirectUrl?: string;
|
redirectUrl?: string;
|
||||||
organizationId?: string;
|
organizationId?: string;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -48,7 +48,9 @@ describe("EmailChangeSignIn", () => {
|
|||||||
expect(screen.getByText("auth.email-change.email_change_success_description")).toBeInTheDocument();
|
expect(screen.getByText("auth.email-change.email_change_success_description")).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(signOut).toHaveBeenCalledWith({ redirect: false });
|
await waitFor(() => {
|
||||||
|
expect(signOut).toHaveBeenCalledWith({ redirect: false });
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("handles failed email change verification", async () => {
|
test("handles failed email change verification", async () => {
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const ZAuditAction = z.enum([
|
|||||||
"twoFactorRequired",
|
"twoFactorRequired",
|
||||||
"emailVerificationAttempted",
|
"emailVerificationAttempted",
|
||||||
"userSignedOut",
|
"userSignedOut",
|
||||||
|
"passwordReset",
|
||||||
]);
|
]);
|
||||||
export const ZActor = z.enum(["user", "api", "system"]);
|
export const ZActor = z.enum(["user", "api", "system"]);
|
||||||
export const ZAuditStatus = z.enum(["success", "failure"]);
|
export const ZAuditStatus = z.enum(["success", "failure"]);
|
||||||
|
|||||||
@@ -2,10 +2,9 @@ import { ContactAttributeKey, Prisma } from "@prisma/client";
|
|||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { TContactAttributeKey, TContactAttributeKeyType } from "@formbricks/types/contact-attribute-key";
|
import { TContactAttributeKey, TContactAttributeKeyType } from "@formbricks/types/contact-attribute-key";
|
||||||
import { DatabaseError, OperationNotAllowedError } from "@formbricks/types/errors";
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
import { TContactAttributeKeyUpdateInput } from "../types/contact-attribute-keys";
|
import { TContactAttributeKeyUpdateInput } from "../types/contact-attribute-keys";
|
||||||
import {
|
import {
|
||||||
createContactAttributeKey,
|
|
||||||
deleteContactAttributeKey,
|
deleteContactAttributeKey,
|
||||||
getContactAttributeKey,
|
getContactAttributeKey,
|
||||||
updateContactAttributeKey,
|
updateContactAttributeKey,
|
||||||
@@ -101,79 +100,6 @@ describe("getContactAttributeKey", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("createContactAttributeKey", () => {
|
|
||||||
const type: TContactAttributeKeyType = "custom";
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should create and return a new contact attribute key", async () => {
|
|
||||||
const createdAttributeKey = { ...mockContactAttributeKey, id: "new_cak_id", key: mockKey, type };
|
|
||||||
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(5); // Below limit
|
|
||||||
vi.mocked(prisma.contactAttributeKey.create).mockResolvedValue(createdAttributeKey);
|
|
||||||
|
|
||||||
const result = await createContactAttributeKey(mockEnvironmentId, mockKey, type);
|
|
||||||
|
|
||||||
expect(result).toEqual(createdAttributeKey);
|
|
||||||
expect(prisma.contactAttributeKey.count).toHaveBeenCalledWith({
|
|
||||||
where: { environmentId: mockEnvironmentId },
|
|
||||||
});
|
|
||||||
expect(prisma.contactAttributeKey.create).toHaveBeenCalledWith({
|
|
||||||
data: {
|
|
||||||
key: mockKey,
|
|
||||||
name: mockKey, // As per implementation
|
|
||||||
type,
|
|
||||||
environment: { connect: { id: mockEnvironmentId } },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should throw OperationNotAllowedError if max attribute classes reached", async () => {
|
|
||||||
// MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT is mocked to 10
|
|
||||||
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(10);
|
|
||||||
|
|
||||||
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(
|
|
||||||
OperationNotAllowedError
|
|
||||||
);
|
|
||||||
expect(prisma.contactAttributeKey.count).toHaveBeenCalledWith({
|
|
||||||
where: { environmentId: mockEnvironmentId },
|
|
||||||
});
|
|
||||||
expect(prisma.contactAttributeKey.create).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should throw Prisma error if prisma.contactAttributeKey.count fails", async () => {
|
|
||||||
const errorMessage = "Prisma count error";
|
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError(errorMessage, {
|
|
||||||
code: "P1000",
|
|
||||||
clientVersion: "test",
|
|
||||||
});
|
|
||||||
vi.mocked(prisma.contactAttributeKey.count).mockRejectedValue(prismaError);
|
|
||||||
|
|
||||||
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(prismaError);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should throw DatabaseError if Prisma create fails", async () => {
|
|
||||||
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(5); // Below limit
|
|
||||||
const errorMessage = "Prisma create error";
|
|
||||||
vi.mocked(prisma.contactAttributeKey.create).mockRejectedValue(
|
|
||||||
new Prisma.PrismaClientKnownRequestError(errorMessage, { code: "P2000", clientVersion: "test" })
|
|
||||||
);
|
|
||||||
|
|
||||||
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(DatabaseError);
|
|
||||||
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(errorMessage);
|
|
||||||
});
|
|
||||||
|
|
||||||
test("should throw generic error if non-Prisma error occurs during create", async () => {
|
|
||||||
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(5);
|
|
||||||
const errorMessage = "Some other error during create";
|
|
||||||
vi.mocked(prisma.contactAttributeKey.create).mockRejectedValue(new Error(errorMessage));
|
|
||||||
|
|
||||||
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(Error);
|
|
||||||
await expect(createContactAttributeKey(mockEnvironmentId, mockKey, type)).rejects.toThrow(errorMessage);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("deleteContactAttributeKey", () => {
|
describe("deleteContactAttributeKey", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
|||||||
@@ -1,15 +1,10 @@
|
|||||||
import { MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "@/lib/constants";
|
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
import { validateInputs } from "@/lib/utils/validate";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { ZId, ZString } from "@formbricks/types/common";
|
import { ZId } from "@formbricks/types/common";
|
||||||
import {
|
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||||
TContactAttributeKey,
|
import { DatabaseError } from "@formbricks/types/errors";
|
||||||
TContactAttributeKeyType,
|
|
||||||
ZContactAttributeKeyType,
|
|
||||||
} from "@formbricks/types/contact-attribute-key";
|
|
||||||
import { DatabaseError, OperationNotAllowedError } from "@formbricks/types/errors";
|
|
||||||
import {
|
import {
|
||||||
TContactAttributeKeyUpdateInput,
|
TContactAttributeKeyUpdateInput,
|
||||||
ZContactAttributeKeyUpdateInput,
|
ZContactAttributeKeyUpdateInput,
|
||||||
@@ -34,48 +29,6 @@ export const getContactAttributeKey = reactCache(
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
export const createContactAttributeKey = async (
|
|
||||||
environmentId: string,
|
|
||||||
key: string,
|
|
||||||
type: TContactAttributeKeyType
|
|
||||||
): Promise<TContactAttributeKey | null> => {
|
|
||||||
validateInputs([environmentId, ZId], [key, ZString], [type, ZContactAttributeKeyType]);
|
|
||||||
|
|
||||||
const contactAttributeKeysCount = await prisma.contactAttributeKey.count({
|
|
||||||
where: {
|
|
||||||
environmentId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (contactAttributeKeysCount >= MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT) {
|
|
||||||
throw new OperationNotAllowedError(
|
|
||||||
`Maximum number of attribute classes (${MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT}) reached for environment ${environmentId}`
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const contactAttributeKey = await prisma.contactAttributeKey.create({
|
|
||||||
data: {
|
|
||||||
key,
|
|
||||||
name: key,
|
|
||||||
type,
|
|
||||||
environment: {
|
|
||||||
connect: {
|
|
||||||
id: environmentId,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return contactAttributeKey;
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
|
||||||
throw new DatabaseError(error.message);
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const deleteContactAttributeKey = async (
|
export const deleteContactAttributeKey = async (
|
||||||
contactAttributeKeyId: string
|
contactAttributeKeyId: string
|
||||||
): Promise<TContactAttributeKey> => {
|
): Promise<TContactAttributeKey> => {
|
||||||
@@ -110,6 +63,8 @@ export const updateContactAttributeKey = async (
|
|||||||
},
|
},
|
||||||
data: {
|
data: {
|
||||||
description: data.description,
|
description: data.description,
|
||||||
|
name: data.name,
|
||||||
|
key: data.key,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ export const ZContactAttributeKeyCreateInput = z.object({
|
|||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
type: z.enum(["custom"]),
|
type: z.enum(["custom"]),
|
||||||
environmentId: z.string(),
|
environmentId: z.string(),
|
||||||
|
name: z.string().optional(),
|
||||||
});
|
});
|
||||||
export type TContactAttributeKeyCreateInput = z.infer<typeof ZContactAttributeKeyCreateInput>;
|
export type TContactAttributeKeyCreateInput = z.infer<typeof ZContactAttributeKeyCreateInput>;
|
||||||
|
|
||||||
export const ZContactAttributeKeyUpdateInput = z.object({
|
export const ZContactAttributeKeyUpdateInput = z.object({
|
||||||
description: z.string().optional(),
|
description: z.string().optional(),
|
||||||
name: z.string().optional(),
|
name: z.string().optional(),
|
||||||
|
key: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export type TContactAttributeKeyUpdateInput = z.infer<typeof ZContactAttributeKeyUpdateInput>;
|
export type TContactAttributeKeyUpdateInput = z.infer<typeof ZContactAttributeKeyUpdateInput>;
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "@/lib/constants";
|
import { MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "@/lib/constants";
|
||||||
|
import { TContactAttributeKeyCreateInput } from "@/modules/ee/contacts/api/v1/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { beforeEach, describe, expect, test, vi } from "vitest";
|
import { beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
@@ -27,8 +28,28 @@ describe("getContactAttributeKeys", () => {
|
|||||||
test("should return contact attribute keys when found", async () => {
|
test("should return contact attribute keys when found", async () => {
|
||||||
const mockEnvironmentIds = ["env1", "env2"];
|
const mockEnvironmentIds = ["env1", "env2"];
|
||||||
const mockAttributeKeys = [
|
const mockAttributeKeys = [
|
||||||
{ id: "key1", environmentId: "env1", name: "Key One", key: "keyOne", type: "custom" },
|
{
|
||||||
{ id: "key2", environmentId: "env2", name: "Key Two", key: "keyTwo", type: "custom" },
|
id: "key1",
|
||||||
|
environmentId: "env1",
|
||||||
|
name: "Key One",
|
||||||
|
key: "keyOne",
|
||||||
|
type: "custom" as TContactAttributeKeyType,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
description: null,
|
||||||
|
isUnique: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "key2",
|
||||||
|
environmentId: "env2",
|
||||||
|
name: "Key Two",
|
||||||
|
key: "keyTwo",
|
||||||
|
type: "custom" as TContactAttributeKeyType,
|
||||||
|
createdAt: new Date(),
|
||||||
|
updatedAt: new Date(),
|
||||||
|
description: null,
|
||||||
|
isUnique: false,
|
||||||
|
},
|
||||||
];
|
];
|
||||||
vi.mocked(prisma.contactAttributeKey.findMany).mockResolvedValue(mockAttributeKeys);
|
vi.mocked(prisma.contactAttributeKey.findMany).mockResolvedValue(mockAttributeKeys);
|
||||||
|
|
||||||
@@ -79,25 +100,31 @@ describe("createContactAttributeKey", () => {
|
|||||||
description: null,
|
description: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const createInput: TContactAttributeKeyCreateInput = {
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
environmentId,
|
||||||
|
name: key,
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should create and return a new contact attribute key", async () => {
|
test("should create and return a new contact attribute key", async () => {
|
||||||
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(0);
|
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(0);
|
||||||
vi.mocked(prisma.contactAttributeKey.create).mockResolvedValue({
|
vi.mocked(prisma.contactAttributeKey.create).mockResolvedValue(mockCreatedAttributeKey);
|
||||||
...mockCreatedAttributeKey,
|
|
||||||
description: null, // ensure description is explicitly null if that's the case
|
|
||||||
});
|
|
||||||
|
|
||||||
const result = await createContactAttributeKey(environmentId, key, type);
|
const result = await createContactAttributeKey(environmentId, createInput);
|
||||||
|
|
||||||
expect(prisma.contactAttributeKey.count).toHaveBeenCalledWith({ where: { environmentId } });
|
expect(prisma.contactAttributeKey.count).toHaveBeenCalledWith({ where: { environmentId } });
|
||||||
expect(prisma.contactAttributeKey.create).toHaveBeenCalledWith({
|
expect(prisma.contactAttributeKey.create).toHaveBeenCalledWith({
|
||||||
data: {
|
data: {
|
||||||
key,
|
key: createInput.key,
|
||||||
name: key,
|
name: createInput.name || createInput.key,
|
||||||
type,
|
type: createInput.type,
|
||||||
|
description: createInput.description || "",
|
||||||
environment: { connect: { id: environmentId } },
|
environment: { connect: { id: environmentId } },
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -107,7 +134,7 @@ describe("createContactAttributeKey", () => {
|
|||||||
test("should throw OperationNotAllowedError if max attribute classes reached", async () => {
|
test("should throw OperationNotAllowedError if max attribute classes reached", async () => {
|
||||||
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT);
|
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT);
|
||||||
|
|
||||||
await expect(createContactAttributeKey(environmentId, key, type)).rejects.toThrow(
|
await expect(createContactAttributeKey(environmentId, createInput)).rejects.toThrow(
|
||||||
OperationNotAllowedError
|
OperationNotAllowedError
|
||||||
);
|
);
|
||||||
expect(prisma.contactAttributeKey.count).toHaveBeenCalledWith({ where: { environmentId } });
|
expect(prisma.contactAttributeKey.count).toHaveBeenCalledWith({ where: { environmentId } });
|
||||||
@@ -121,8 +148,8 @@ describe("createContactAttributeKey", () => {
|
|||||||
new Prisma.PrismaClientKnownRequestError(errorMessage, { code: "P2000", clientVersion: "test" })
|
new Prisma.PrismaClientKnownRequestError(errorMessage, { code: "P2000", clientVersion: "test" })
|
||||||
);
|
);
|
||||||
|
|
||||||
await expect(createContactAttributeKey(environmentId, key, type)).rejects.toThrow(DatabaseError);
|
await expect(createContactAttributeKey(environmentId, createInput)).rejects.toThrow(DatabaseError);
|
||||||
await expect(createContactAttributeKey(environmentId, key, type)).rejects.toThrow(errorMessage);
|
await expect(createContactAttributeKey(environmentId, createInput)).rejects.toThrow(errorMessage);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("should throw generic error if non-Prisma error occurs during create", async () => {
|
test("should throw generic error if non-Prisma error occurs during create", async () => {
|
||||||
@@ -130,7 +157,55 @@ describe("createContactAttributeKey", () => {
|
|||||||
const errorMessage = "Some other create error";
|
const errorMessage = "Some other create error";
|
||||||
vi.mocked(prisma.contactAttributeKey.create).mockRejectedValue(new Error(errorMessage));
|
vi.mocked(prisma.contactAttributeKey.create).mockRejectedValue(new Error(errorMessage));
|
||||||
|
|
||||||
await expect(createContactAttributeKey(environmentId, key, type)).rejects.toThrow(Error);
|
await expect(createContactAttributeKey(environmentId, createInput)).rejects.toThrow(Error);
|
||||||
await expect(createContactAttributeKey(environmentId, key, type)).rejects.toThrow(errorMessage);
|
await expect(createContactAttributeKey(environmentId, createInput)).rejects.toThrow(errorMessage);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should use key as name when name is not provided", async () => {
|
||||||
|
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(0);
|
||||||
|
vi.mocked(prisma.contactAttributeKey.create).mockResolvedValue(mockCreatedAttributeKey);
|
||||||
|
|
||||||
|
const inputWithoutName: TContactAttributeKeyCreateInput = {
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
environmentId,
|
||||||
|
description: "",
|
||||||
|
};
|
||||||
|
|
||||||
|
await createContactAttributeKey(environmentId, inputWithoutName);
|
||||||
|
|
||||||
|
expect(prisma.contactAttributeKey.create).toHaveBeenCalledWith({
|
||||||
|
data: {
|
||||||
|
key: inputWithoutName.key,
|
||||||
|
name: inputWithoutName.key, // Should fall back to key when name is not provided
|
||||||
|
type: inputWithoutName.type,
|
||||||
|
description: inputWithoutName.description || "",
|
||||||
|
environment: { connect: { id: environmentId } },
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("should use empty string for description when description is not provided", async () => {
|
||||||
|
vi.mocked(prisma.contactAttributeKey.count).mockResolvedValue(0);
|
||||||
|
vi.mocked(prisma.contactAttributeKey.create).mockResolvedValue(mockCreatedAttributeKey);
|
||||||
|
|
||||||
|
const inputWithoutDescription: TContactAttributeKeyCreateInput = {
|
||||||
|
key,
|
||||||
|
type,
|
||||||
|
environmentId,
|
||||||
|
name: "Test Name",
|
||||||
|
};
|
||||||
|
|
||||||
|
await createContactAttributeKey(environmentId, inputWithoutDescription);
|
||||||
|
|
||||||
|
expect(prisma.contactAttributeKey.create).toHaveBeenCalledWith({
|
||||||
|
data: {
|
||||||
|
key: inputWithoutDescription.key,
|
||||||
|
name: inputWithoutDescription.name,
|
||||||
|
type: inputWithoutDescription.type,
|
||||||
|
description: "", // Should fall back to empty string when description is not provided
|
||||||
|
environment: { connect: { id: environmentId } },
|
||||||
|
},
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,14 +1,10 @@
|
|||||||
import { MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "@/lib/constants";
|
import { MAX_ATTRIBUTE_CLASSES_PER_ENVIRONMENT } from "@/lib/constants";
|
||||||
import { validateInputs } from "@/lib/utils/validate";
|
import { TContactAttributeKeyCreateInput } from "@/modules/ee/contacts/api/v1/management/contact-attribute-keys/[contactAttributeKeyId]/types/contact-attribute-keys";
|
||||||
import { Prisma } from "@prisma/client";
|
import { Prisma } from "@prisma/client";
|
||||||
import { cache as reactCache } from "react";
|
import { cache as reactCache } from "react";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
import { ZId, ZString } from "@formbricks/types/common";
|
import { PrismaErrorType } from "@formbricks/database/types/error";
|
||||||
import {
|
import { TContactAttributeKey } from "@formbricks/types/contact-attribute-key";
|
||||||
TContactAttributeKey,
|
|
||||||
TContactAttributeKeyType,
|
|
||||||
ZContactAttributeKeyType,
|
|
||||||
} from "@formbricks/types/contact-attribute-key";
|
|
||||||
import { DatabaseError, OperationNotAllowedError } from "@formbricks/types/errors";
|
import { DatabaseError, OperationNotAllowedError } from "@formbricks/types/errors";
|
||||||
|
|
||||||
export const getContactAttributeKeys = reactCache(
|
export const getContactAttributeKeys = reactCache(
|
||||||
@@ -30,11 +26,8 @@ export const getContactAttributeKeys = reactCache(
|
|||||||
|
|
||||||
export const createContactAttributeKey = async (
|
export const createContactAttributeKey = async (
|
||||||
environmentId: string,
|
environmentId: string,
|
||||||
key: string,
|
data: TContactAttributeKeyCreateInput
|
||||||
type: TContactAttributeKeyType
|
|
||||||
): Promise<TContactAttributeKey | null> => {
|
): Promise<TContactAttributeKey | null> => {
|
||||||
validateInputs([environmentId, ZId], [key, ZString], [type, ZContactAttributeKeyType]);
|
|
||||||
|
|
||||||
const contactAttributeKeysCount = await prisma.contactAttributeKey.count({
|
const contactAttributeKeysCount = await prisma.contactAttributeKey.count({
|
||||||
where: {
|
where: {
|
||||||
environmentId,
|
environmentId,
|
||||||
@@ -50,9 +43,10 @@ export const createContactAttributeKey = async (
|
|||||||
try {
|
try {
|
||||||
const contactAttributeKey = await prisma.contactAttributeKey.create({
|
const contactAttributeKey = await prisma.contactAttributeKey.create({
|
||||||
data: {
|
data: {
|
||||||
key,
|
key: data.key,
|
||||||
name: key,
|
name: data.name ?? data.key,
|
||||||
type,
|
type: data.type,
|
||||||
|
description: data.description ?? "",
|
||||||
environment: {
|
environment: {
|
||||||
connect: {
|
connect: {
|
||||||
id: environmentId,
|
id: environmentId,
|
||||||
@@ -64,6 +58,10 @@ export const createContactAttributeKey = async (
|
|||||||
return contactAttributeKey;
|
return contactAttributeKey;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||||
|
if (error.code === PrismaErrorType.UniqueConstraintViolation) {
|
||||||
|
throw new DatabaseError("Attribute key already exists");
|
||||||
|
}
|
||||||
|
|
||||||
throw new DatabaseError(error.message);
|
throw new DatabaseError(error.message);
|
||||||
}
|
}
|
||||||
throw error;
|
throw error;
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ export const POST = withApiLogging(
|
|||||||
),
|
),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const environmentId = contactAttibuteKeyInput.environmentId;
|
const environmentId = inputValidation.data.environmentId;
|
||||||
auditLog.organizationId = authentication.organizationId;
|
auditLog.organizationId = authentication.organizationId;
|
||||||
|
|
||||||
if (!hasPermission(authentication.environmentPermissions, environmentId, "POST")) {
|
if (!hasPermission(authentication.environmentPermissions, environmentId, "POST")) {
|
||||||
@@ -84,11 +84,7 @@ export const POST = withApiLogging(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const contactAttributeKey = await createContactAttributeKey(
|
const contactAttributeKey = await createContactAttributeKey(environmentId, inputValidation.data);
|
||||||
environmentId,
|
|
||||||
inputValidation.data.key,
|
|
||||||
inputValidation.data.type
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!contactAttributeKey) {
|
if (!contactAttributeKey) {
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ const bulkContactEndpoint: ZodOpenApiOperationObject = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
tags: ["Management API > Contacts"],
|
tags: ["Management API - Contacts"],
|
||||||
responses: {
|
responses: {
|
||||||
"200": {
|
"200": {
|
||||||
description: "Contacts uploaded successfully.",
|
description: "Contacts uploaded successfully.",
|
||||||
|
|||||||
@@ -170,7 +170,7 @@ export function TargetingCard({
|
|||||||
<Collapsible.CollapsibleTrigger
|
<Collapsible.CollapsibleTrigger
|
||||||
asChild
|
asChild
|
||||||
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
|
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
|
||||||
<div className="inline-flex px-4 py-6">
|
<div className="inline-flex px-4 py-4">
|
||||||
<div className="flex items-center pl-2 pr-5">
|
<div className="flex items-center pl-2 pr-5">
|
||||||
<CheckIcon
|
<CheckIcon
|
||||||
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
|
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
|
||||||
|
|||||||
@@ -60,11 +60,7 @@ const getRatingContent = (scale: string, i: number, range: number, isColorCoding
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (scale === "number") {
|
if (scale === "number") {
|
||||||
return (
|
return <Text className="m-0 h-[44px] text-center text-[14px] leading-[44px]">{i + 1}</Text>;
|
||||||
<Text className="m-0 h-[44px] text-center text-[14px] leading-[44px]">
|
|
||||||
{i + 1}
|
|
||||||
</Text>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
if (scale === "star") {
|
if (scale === "star") {
|
||||||
return <Text className="m-auto text-3xl">⭐</Text>;
|
return <Text className="m-auto text-3xl">⭐</Text>;
|
||||||
@@ -232,8 +228,8 @@ export async function PreviewEmailTemplate({
|
|||||||
{ "rounded-l-lg border-l": i === 0 },
|
{ "rounded-l-lg border-l": i === 0 },
|
||||||
{ "rounded-r-lg": i === firstQuestion.range - 1 },
|
{ "rounded-r-lg": i === firstQuestion.range - 1 },
|
||||||
firstQuestion.isColorCodingEnabled &&
|
firstQuestion.isColorCodingEnabled &&
|
||||||
firstQuestion.scale === "number" &&
|
firstQuestion.scale === "number" &&
|
||||||
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
|
`border border-t-[6px] border-t-${getRatingNumberOptionColor(firstQuestion.range, i + 1)}`,
|
||||||
firstQuestion.scale === "star" && "border-transparent"
|
firstQuestion.scale === "star" && "border-transparent"
|
||||||
)}
|
)}
|
||||||
href={`${urlWithPrefilling}${firstQuestion.id}=${(i + 1).toString()}`}
|
href={`${urlWithPrefilling}${firstQuestion.id}=${(i + 1).toString()}`}
|
||||||
|
|||||||
@@ -89,6 +89,31 @@ describe("RecallItemSelect", () => {
|
|||||||
expect(screen.queryByText("_File Upload Question_")).not.toBeInTheDocument();
|
expect(screen.queryByText("_File Upload Question_")).not.toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("do not render questions if questionId is 'start' (welcome card)", async () => {
|
||||||
|
render(
|
||||||
|
<RecallItemSelect
|
||||||
|
localSurvey={mockSurvey}
|
||||||
|
questionId="start"
|
||||||
|
addRecallItem={mockAddRecallItem}
|
||||||
|
setShowRecallItemSelect={mockSetShowRecallItemSelect}
|
||||||
|
recallItems={mockRecallItems}
|
||||||
|
selectedLanguageCode="en"
|
||||||
|
hiddenFields={mockSurvey.hiddenFields}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(screen.queryByText("_Question 1_")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("_Question 2_")).not.toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.getByText("_hidden1_")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("_hidden2_")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("_Variable 1_")).toBeInTheDocument();
|
||||||
|
expect(screen.getByText("_Variable 2_")).toBeInTheDocument();
|
||||||
|
|
||||||
|
expect(screen.queryByText("_Current Question_")).not.toBeInTheDocument();
|
||||||
|
expect(screen.queryByText("_File Upload Question_")).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
test("filters recall items based on search input", async () => {
|
test("filters recall items based on search input", async () => {
|
||||||
const user = userEvent.setup();
|
const user = userEvent.setup();
|
||||||
render(
|
render(
|
||||||
|
|||||||
@@ -109,6 +109,9 @@ export const RecallItemSelect = ({
|
|||||||
}, [localSurvey.variables, recallItemIds]);
|
}, [localSurvey.variables, recallItemIds]);
|
||||||
|
|
||||||
const surveyQuestionRecallItems = useMemo(() => {
|
const surveyQuestionRecallItems = useMemo(() => {
|
||||||
|
const isWelcomeCard = questionId === "start";
|
||||||
|
if (isWelcomeCard) return [];
|
||||||
|
|
||||||
const isEndingCard = !localSurvey.questions.map((question) => question.id).includes(questionId);
|
const isEndingCard = !localSurvey.questions.map((question) => question.id).includes(questionId);
|
||||||
const idx = isEndingCard
|
const idx = isEndingCard
|
||||||
? localSurvey.questions.length
|
? localSurvey.questions.length
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ export const QuestionFormInput = ({
|
|||||||
onAddFallback={() => {
|
onAddFallback={() => {
|
||||||
inputRef.current?.focus();
|
inputRef.current?.focus();
|
||||||
}}
|
}}
|
||||||
isRecallAllowed={!isWelcomeCard && (id === "headline" || id === "subheader")}
|
isRecallAllowed={id === "headline" || id === "subheader"}
|
||||||
usedLanguageCode={usedLanguageCode}
|
usedLanguageCode={usedLanguageCode}
|
||||||
render={({
|
render={({
|
||||||
value,
|
value,
|
||||||
|
|||||||
@@ -1,105 +0,0 @@
|
|||||||
import "@testing-library/jest-dom/vitest";
|
|
||||||
import { afterEach, describe, expect, test, vi } from "vitest";
|
|
||||||
import { prisma } from "@formbricks/database";
|
|
||||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
|
||||||
import { subscribeOrganizationMembersToSurveyResponses } from "./organization";
|
|
||||||
import { updateUser } from "./user";
|
|
||||||
|
|
||||||
vi.mock("@formbricks/database", () => ({
|
|
||||||
prisma: {
|
|
||||||
user: {
|
|
||||||
findUnique: vi.fn(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("./user", () => ({
|
|
||||||
updateUser: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe("subscribeOrganizationMembersToSurveyResponses", () => {
|
|
||||||
afterEach(() => {
|
|
||||||
vi.clearAllMocks();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("subscribes user to survey responses successfully", async () => {
|
|
||||||
const mockUser = {
|
|
||||||
id: "user-123",
|
|
||||||
notificationSettings: {
|
|
||||||
alert: { "existing-survey-id": true },
|
|
||||||
weeklySummary: {},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const surveyId = "survey-123";
|
|
||||||
const userId = "user-123";
|
|
||||||
|
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce(mockUser);
|
|
||||||
vi.mocked(updateUser).mockResolvedValueOnce({} as any);
|
|
||||||
|
|
||||||
await subscribeOrganizationMembersToSurveyResponses(surveyId, userId);
|
|
||||||
|
|
||||||
expect(prisma.user.findUnique).toHaveBeenCalledWith({
|
|
||||||
where: { id: userId },
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(updateUser).toHaveBeenCalledWith(userId, {
|
|
||||||
notificationSettings: {
|
|
||||||
alert: {
|
|
||||||
"existing-survey-id": true,
|
|
||||||
"survey-123": true,
|
|
||||||
},
|
|
||||||
weeklySummary: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("creates notification settings if user doesn't have any", async () => {
|
|
||||||
const mockUser = {
|
|
||||||
id: "user-123",
|
|
||||||
notificationSettings: null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const surveyId = "survey-123";
|
|
||||||
const userId = "user-123";
|
|
||||||
|
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce(mockUser);
|
|
||||||
vi.mocked(updateUser).mockResolvedValueOnce({} as any);
|
|
||||||
|
|
||||||
await subscribeOrganizationMembersToSurveyResponses(surveyId, userId);
|
|
||||||
|
|
||||||
expect(updateUser).toHaveBeenCalledWith(userId, {
|
|
||||||
notificationSettings: {
|
|
||||||
alert: {
|
|
||||||
"survey-123": true,
|
|
||||||
},
|
|
||||||
weeklySummary: {},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
test("throws ResourceNotFoundError if user is not found", async () => {
|
|
||||||
const surveyId = "survey-123";
|
|
||||||
const userId = "nonexistent-user";
|
|
||||||
|
|
||||||
vi.mocked(prisma.user.findUnique).mockResolvedValueOnce(null);
|
|
||||||
|
|
||||||
await expect(subscribeOrganizationMembersToSurveyResponses(surveyId, userId)).rejects.toThrow(
|
|
||||||
new ResourceNotFoundError("User", userId)
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(updateUser).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
test("propagates errors from database operations", async () => {
|
|
||||||
const surveyId = "survey-123";
|
|
||||||
const userId = "user-123";
|
|
||||||
const dbError = new Error("Database error");
|
|
||||||
|
|
||||||
vi.mocked(prisma.user.findUnique).mockRejectedValueOnce(dbError);
|
|
||||||
|
|
||||||
await expect(subscribeOrganizationMembersToSurveyResponses(surveyId, userId)).rejects.toThrow(dbError);
|
|
||||||
|
|
||||||
expect(updateUser).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import { updateUser } from "@/modules/survey/components/template-list/lib/user";
|
|
||||||
import { prisma } from "@formbricks/database";
|
|
||||||
import { ResourceNotFoundError } from "@formbricks/types/errors";
|
|
||||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
|
||||||
|
|
||||||
export const subscribeOrganizationMembersToSurveyResponses = async (
|
|
||||||
surveyId: string,
|
|
||||||
createdBy: string
|
|
||||||
): Promise<void> => {
|
|
||||||
try {
|
|
||||||
const surveyCreator = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
id: createdBy,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!surveyCreator) {
|
|
||||||
throw new ResourceNotFoundError("User", createdBy);
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultSettings = { alert: {}, weeklySummary: {} };
|
|
||||||
const updatedNotificationSettings: TUserNotificationSettings = {
|
|
||||||
...defaultSettings,
|
|
||||||
...surveyCreator.notificationSettings,
|
|
||||||
};
|
|
||||||
|
|
||||||
updatedNotificationSettings.alert[surveyId] = true;
|
|
||||||
|
|
||||||
await updateUser(surveyCreator.id, {
|
|
||||||
notificationSettings: updatedNotificationSettings,
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
@@ -1,7 +1,9 @@
|
|||||||
|
import {
|
||||||
|
getOrganizationByEnvironmentId,
|
||||||
|
subscribeOrganizationMembersToSurveyResponses,
|
||||||
|
} from "@/lib/organization/service";
|
||||||
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
|
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
|
||||||
import { subscribeOrganizationMembersToSurveyResponses } from "@/modules/survey/components/template-list/lib/organization";
|
|
||||||
import { getActionClasses } from "@/modules/survey/lib/action-class";
|
import { getActionClasses } from "@/modules/survey/lib/action-class";
|
||||||
import { getOrganizationAIKeys, getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization";
|
|
||||||
import { selectSurvey } from "@/modules/survey/lib/survey";
|
import { selectSurvey } from "@/modules/survey/lib/survey";
|
||||||
import { ActionClass, Prisma } from "@prisma/client";
|
import { ActionClass, Prisma } from "@prisma/client";
|
||||||
import "@testing-library/jest-dom/vitest";
|
import "@testing-library/jest-dom/vitest";
|
||||||
@@ -21,19 +23,15 @@ vi.mock("@/lib/survey/utils", () => ({
|
|||||||
checkForInvalidImagesInQuestions: vi.fn(),
|
checkForInvalidImagesInQuestions: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/modules/survey/components/template-list/lib/organization", () => ({
|
vi.mock("@/lib/organization/service", () => ({
|
||||||
subscribeOrganizationMembersToSurveyResponses: vi.fn(),
|
subscribeOrganizationMembersToSurveyResponses: vi.fn(),
|
||||||
|
getOrganizationByEnvironmentId: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/modules/survey/lib/action-class", () => ({
|
vi.mock("@/modules/survey/lib/action-class", () => ({
|
||||||
getActionClasses: vi.fn(),
|
getActionClasses: vi.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
vi.mock("@/modules/survey/lib/organization", () => ({
|
|
||||||
getOrganizationIdFromEnvironmentId: vi.fn(),
|
|
||||||
getOrganizationAIKeys: vi.fn(),
|
|
||||||
}));
|
|
||||||
|
|
||||||
vi.mock("@/modules/survey/lib/survey", () => ({
|
vi.mock("@/modules/survey/lib/survey", () => ({
|
||||||
selectSurvey: {
|
selectSurvey: {
|
||||||
id: true,
|
id: true,
|
||||||
@@ -86,8 +84,7 @@ describe("survey module", () => {
|
|||||||
// Mock dependencies
|
// Mock dependencies
|
||||||
const mockActionClasses: ActionClass[] = [];
|
const mockActionClasses: ActionClass[] = [];
|
||||||
vi.mocked(getActionClasses).mockResolvedValue(mockActionClasses);
|
vi.mocked(getActionClasses).mockResolvedValue(mockActionClasses);
|
||||||
vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValue("org-123");
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue({ id: "org-123", name: "Org" } as any);
|
||||||
vi.mocked(getOrganizationAIKeys).mockResolvedValue({ id: "org-123", name: "Org" } as any);
|
|
||||||
|
|
||||||
const mockCreatedSurvey = {
|
const mockCreatedSurvey = {
|
||||||
id: "survey-123",
|
id: "survey-123",
|
||||||
@@ -109,8 +106,7 @@ describe("survey module", () => {
|
|||||||
|
|
||||||
// Verify results
|
// Verify results
|
||||||
expect(getActionClasses).toHaveBeenCalledWith(environmentId);
|
expect(getActionClasses).toHaveBeenCalledWith(environmentId);
|
||||||
expect(getOrganizationIdFromEnvironmentId).toHaveBeenCalledWith(environmentId);
|
expect(getOrganizationByEnvironmentId).toHaveBeenCalledWith(environmentId);
|
||||||
expect(getOrganizationAIKeys).toHaveBeenCalledWith("org-123");
|
|
||||||
expect(prisma.survey.create).toHaveBeenCalledWith({
|
expect(prisma.survey.create).toHaveBeenCalledWith({
|
||||||
data: expect.objectContaining({
|
data: expect.objectContaining({
|
||||||
name: surveyBody.name,
|
name: surveyBody.name,
|
||||||
@@ -122,7 +118,11 @@ describe("survey module", () => {
|
|||||||
});
|
});
|
||||||
expect(prisma.segment.create).toHaveBeenCalled();
|
expect(prisma.segment.create).toHaveBeenCalled();
|
||||||
expect(prisma.survey.update).toHaveBeenCalled();
|
expect(prisma.survey.update).toHaveBeenCalled();
|
||||||
expect(subscribeOrganizationMembersToSurveyResponses).toHaveBeenCalledWith("survey-123", "user-123");
|
expect(subscribeOrganizationMembersToSurveyResponses).toHaveBeenCalledWith(
|
||||||
|
"survey-123",
|
||||||
|
"user-123",
|
||||||
|
"org-123"
|
||||||
|
);
|
||||||
expect(capturePosthogEnvironmentEvent).toHaveBeenCalledWith(
|
expect(capturePosthogEnvironmentEvent).toHaveBeenCalledWith(
|
||||||
environmentId,
|
environmentId,
|
||||||
"survey created",
|
"survey created",
|
||||||
@@ -143,8 +143,7 @@ describe("survey module", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(getActionClasses).mockResolvedValue([]);
|
vi.mocked(getActionClasses).mockResolvedValue([]);
|
||||||
vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValue("org-123");
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue({ id: "org-123" } as any);
|
||||||
vi.mocked(getOrganizationAIKeys).mockResolvedValue({ id: "org-123" } as any);
|
|
||||||
vi.mocked(prisma.survey.create).mockResolvedValue({
|
vi.mocked(prisma.survey.create).mockResolvedValue({
|
||||||
id: "survey-123",
|
id: "survey-123",
|
||||||
environmentId,
|
environmentId,
|
||||||
@@ -172,8 +171,7 @@ describe("survey module", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(getActionClasses).mockResolvedValue([]);
|
vi.mocked(getActionClasses).mockResolvedValue([]);
|
||||||
vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValue("org-123");
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue({ id: "org-123" } as any);
|
||||||
vi.mocked(getOrganizationAIKeys).mockResolvedValue({ id: "org-123" } as any);
|
|
||||||
vi.mocked(prisma.survey.create).mockResolvedValue({
|
vi.mocked(prisma.survey.create).mockResolvedValue({
|
||||||
id: "survey-123",
|
id: "survey-123",
|
||||||
environmentId,
|
environmentId,
|
||||||
@@ -204,8 +202,7 @@ describe("survey module", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(getActionClasses).mockResolvedValue([]);
|
vi.mocked(getActionClasses).mockResolvedValue([]);
|
||||||
vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValue("org-123");
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue(null);
|
||||||
vi.mocked(getOrganizationAIKeys).mockResolvedValue(null);
|
|
||||||
|
|
||||||
await expect(createSurvey(environmentId, surveyBody)).rejects.toThrow(ResourceNotFoundError);
|
await expect(createSurvey(environmentId, surveyBody)).rejects.toThrow(ResourceNotFoundError);
|
||||||
});
|
});
|
||||||
@@ -220,12 +217,11 @@ describe("survey module", () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
vi.mocked(getActionClasses).mockResolvedValue([]);
|
vi.mocked(getActionClasses).mockResolvedValue([]);
|
||||||
vi.mocked(getOrganizationIdFromEnvironmentId).mockResolvedValue("org-123");
|
vi.mocked(getOrganizationByEnvironmentId).mockResolvedValue({ id: "org-123" } as any);
|
||||||
vi.mocked(getOrganizationAIKeys).mockResolvedValue({ id: "org-123" } as any);
|
|
||||||
|
|
||||||
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
const prismaError = new Prisma.PrismaClientKnownRequestError("Database error", {
|
||||||
code: "P2002",
|
code: "P2002",
|
||||||
clientVersion: "4.8.0",
|
clientVersion: "5.0.0",
|
||||||
});
|
});
|
||||||
vi.mocked(prisma.survey.create).mockRejectedValue(prismaError);
|
vi.mocked(prisma.survey.create).mockRejectedValue(prismaError);
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
|
import {
|
||||||
|
getOrganizationByEnvironmentId,
|
||||||
|
subscribeOrganizationMembersToSurveyResponses,
|
||||||
|
} from "@/lib/organization/service";
|
||||||
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
|
import { capturePosthogEnvironmentEvent } from "@/lib/posthogServer";
|
||||||
import { checkForInvalidImagesInQuestions } from "@/lib/survey/utils";
|
import { checkForInvalidImagesInQuestions } from "@/lib/survey/utils";
|
||||||
import { subscribeOrganizationMembersToSurveyResponses } from "@/modules/survey/components/template-list/lib/organization";
|
|
||||||
import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger";
|
import { TriggerUpdate } from "@/modules/survey/editor/types/survey-trigger";
|
||||||
import { getActionClasses } from "@/modules/survey/lib/action-class";
|
import { getActionClasses } from "@/modules/survey/lib/action-class";
|
||||||
import { getOrganizationAIKeys, getOrganizationIdFromEnvironmentId } from "@/modules/survey/lib/organization";
|
|
||||||
import { selectSurvey } from "@/modules/survey/lib/survey";
|
import { selectSurvey } from "@/modules/survey/lib/survey";
|
||||||
import { ActionClass, Prisma } from "@prisma/client";
|
import { ActionClass, Prisma } from "@prisma/client";
|
||||||
import { prisma } from "@formbricks/database";
|
import { prisma } from "@formbricks/database";
|
||||||
@@ -43,8 +45,7 @@ export const createSurvey = async (
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizationId = await getOrganizationIdFromEnvironmentId(environmentId);
|
const organization = await getOrganizationByEnvironmentId(environmentId);
|
||||||
const organization = await getOrganizationAIKeys(organizationId);
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
throw new ResourceNotFoundError("Organization", null);
|
throw new ResourceNotFoundError("Organization", null);
|
||||||
}
|
}
|
||||||
@@ -118,7 +119,7 @@ export const createSurvey = async (
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (createdBy) {
|
if (createdBy) {
|
||||||
await subscribeOrganizationMembersToSurveyResponses(survey.id, createdBy);
|
await subscribeOrganizationMembersToSurveyResponses(survey.id, createdBy, organization.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
await capturePosthogEnvironmentEvent(survey.environmentId, "survey created", {
|
await capturePosthogEnvironmentEvent(survey.environmentId, "survey created", {
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ export const TargetingLockedCard = ({ isFormbricksCloud, environmentId }: Target
|
|||||||
<Collapsible.CollapsibleTrigger
|
<Collapsible.CollapsibleTrigger
|
||||||
asChild
|
asChild
|
||||||
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
|
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
|
||||||
<div className="inline-flex px-4 py-6">
|
<div className="inline-flex px-4 py-4">
|
||||||
<div className="flex items-center pl-2 pr-5">
|
<div className="flex items-center pl-2 pr-5">
|
||||||
<div className="rounded-full border border-slate-300 bg-slate-100 p-1">
|
<div className="rounded-full border border-slate-300 bg-slate-100 p-1">
|
||||||
<LockIcon className="h-4 w-4 text-slate-500" strokeWidth={3} />
|
<LockIcon className="h-4 w-4 text-slate-500" strokeWidth={3} />
|
||||||
|
|||||||
@@ -195,9 +195,8 @@ export const SurveyDropDownMenu = ({
|
|||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
setIsDropDownOpen(false);
|
setIsDropDownOpen(false);
|
||||||
const newId = await refreshSingleUseId();
|
const newId = await refreshSingleUseId();
|
||||||
const previewUrl = newId
|
const previewUrl =
|
||||||
? `/s/${survey.id}?suId=${newId}&preview=true`
|
surveyLink + (newId ? `?suId=${newId}&preview=true` : "?preview=true");
|
||||||
: `/s/${survey.id}?preview=true`;
|
|
||||||
window.open(previewUrl, "_blank");
|
window.open(previewUrl, "_blank");
|
||||||
}}>
|
}}>
|
||||||
<EyeIcon className="mr-2 h-4 w-4" />
|
<EyeIcon className="mr-2 h-4 w-4" />
|
||||||
|
|||||||
@@ -3,14 +3,6 @@ import { render } from "@testing-library/react";
|
|||||||
import { type MockedFunction, beforeEach, describe, expect, test, vi } from "vitest";
|
import { type MockedFunction, beforeEach, describe, expect, test, vi } from "vitest";
|
||||||
import { ClientLogout } from "./index";
|
import { ClientLogout } from "./index";
|
||||||
|
|
||||||
// Mock the localStorage
|
|
||||||
const mockRemoveItem = vi.fn();
|
|
||||||
Object.defineProperty(window, "localStorage", {
|
|
||||||
value: {
|
|
||||||
removeItem: mockRemoveItem,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mock next-auth/react
|
// Mock next-auth/react
|
||||||
const mockSignOut = vi.fn();
|
const mockSignOut = vi.fn();
|
||||||
vi.mock("@/modules/auth/hooks/use-sign-out", () => ({
|
vi.mock("@/modules/auth/hooks/use-sign-out", () => ({
|
||||||
@@ -37,6 +29,7 @@ describe("ClientLogout", () => {
|
|||||||
redirectUrl: "/auth/login",
|
redirectUrl: "/auth/login",
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -50,14 +43,10 @@ describe("ClientLogout", () => {
|
|||||||
redirectUrl: "/auth/login",
|
redirectUrl: "/auth/login",
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test("removes environment ID from localStorage", () => {
|
|
||||||
render(<ClientLogout />);
|
|
||||||
expect(mockRemoveItem).toHaveBeenCalledWith("formbricks-environment-id");
|
|
||||||
});
|
|
||||||
|
|
||||||
test("renders null", () => {
|
test("renders null", () => {
|
||||||
const { container } = render(<ClientLogout />);
|
const { container } = render(<ClientLogout />);
|
||||||
expect(container.firstChild).toBeNull();
|
expect(container.firstChild).toBeNull();
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { FORMBRICKS_ENVIRONMENT_ID_LS } from "@/lib/localStorage";
|
|
||||||
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
import { useSignOut } from "@/modules/auth/hooks/use-sign-out";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
@@ -8,12 +7,12 @@ export const ClientLogout = () => {
|
|||||||
const { signOut: signOutWithAudit } = useSignOut();
|
const { signOut: signOutWithAudit } = useSignOut();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
localStorage.removeItem(FORMBRICKS_ENVIRONMENT_ID_LS);
|
|
||||||
signOutWithAudit({
|
signOutWithAudit({
|
||||||
reason: "forced_logout",
|
reason: "forced_logout",
|
||||||
redirectUrl: "/auth/login",
|
redirectUrl: "/auth/login",
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: "/auth/login",
|
callbackUrl: "/auth/login",
|
||||||
|
clearEnvironmentId: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
return null;
|
return null;
|
||||||
|
|||||||
@@ -397,7 +397,10 @@ export const ToolbarPlugin = (props: TextEditorProps & { container: HTMLElement
|
|||||||
|
|
||||||
editor.registerUpdateListener(({ editorState, prevEditorState }) => {
|
editor.registerUpdateListener(({ editorState, prevEditorState }) => {
|
||||||
editorState.read(() => {
|
editorState.read(() => {
|
||||||
const textInHtml = $generateHtmlFromNodes(editor).replace(/</g, "<").replace(/>/g, ">");
|
const textInHtml = $generateHtmlFromNodes(editor)
|
||||||
|
.replace(/</g, "<")
|
||||||
|
.replace(/>/g, ">")
|
||||||
|
.replace(/white-space:\s*pre-wrap;?/g, "");
|
||||||
setText.current(textInHtml);
|
setText.current(textInHtml);
|
||||||
});
|
});
|
||||||
if (!prevEditorState._selection) editor.blur();
|
if (!prevEditorState._selection) editor.blur();
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ export const IconBar = ({ actions }: IconBarProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="flex items-center justify-center divide-x rounded-lg border border-slate-300 bg-white"
|
className="flex items-center justify-center divide-x rounded-md border border-slate-300 bg-white"
|
||||||
role="toolbar"
|
role="toolbar"
|
||||||
aria-label="Action buttons">
|
aria-label="Action buttons">
|
||||||
{actions
|
{actions
|
||||||
|
|||||||
@@ -21,16 +21,27 @@ export const OptionsSwitch = ({
|
|||||||
const [highlightStyle, setHighlightStyle] = useState({});
|
const [highlightStyle, setHighlightStyle] = useState({});
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (containerRef.current) {
|
const updateHighlight = () => {
|
||||||
const activeElement = containerRef.current.querySelector(`[data-value="${currentOption}"]`);
|
if (containerRef.current) {
|
||||||
if (activeElement) {
|
const activeElement = containerRef.current.querySelector(`[data-value="${currentOption}"]`);
|
||||||
const { offsetLeft, offsetWidth } = activeElement as HTMLElement;
|
if (activeElement) {
|
||||||
setHighlightStyle({
|
const { offsetLeft, offsetWidth } = activeElement as HTMLElement;
|
||||||
left: `${offsetLeft}px`,
|
setHighlightStyle({
|
||||||
width: `${offsetWidth}px`,
|
left: `${offsetLeft}px`,
|
||||||
});
|
width: `${offsetWidth}px`,
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Hide highlight if no matching element found
|
||||||
|
setHighlightStyle({ opacity: 0 });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
// Initial call
|
||||||
|
updateHighlight();
|
||||||
|
|
||||||
|
// Listen to resize
|
||||||
|
window.addEventListener("resize", updateHighlight);
|
||||||
|
return () => window.removeEventListener("resize", updateHighlight);
|
||||||
}, [currentOption]);
|
}, [currentOption]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ const v1ClientEndpoints = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
summary: "Update Response",
|
summary: "Update Response",
|
||||||
tags: ["Client API > Response"],
|
tags: ["Client API - Response"],
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: "https://app.formbricks.com/api/v2",
|
url: "https://app.formbricks.com/api/v2",
|
||||||
@@ -95,7 +95,7 @@ const v1ClientEndpoints = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
summary: "Create Response",
|
summary: "Create Response",
|
||||||
tags: ["Client API > Response"],
|
tags: ["Client API - Response"],
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: "https://app.formbricks.com/api/v2",
|
url: "https://app.formbricks.com/api/v2",
|
||||||
@@ -145,7 +145,7 @@ const v1ClientEndpoints = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
summary: "Update Contact (Attributes)",
|
summary: "Update Contact (Attributes)",
|
||||||
tags: ["Client API > Contacts"],
|
tags: ["Client API - Contacts"],
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: "https://app.formbricks.com/api/v2",
|
url: "https://app.formbricks.com/api/v2",
|
||||||
@@ -175,7 +175,7 @@ const v1ClientEndpoints = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
summary: "Get Contact State",
|
summary: "Get Contact State",
|
||||||
tags: ["Client API > Contacts"],
|
tags: ["Client API - Contacts"],
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: "https://app.formbricks.com/api/v2",
|
url: "https://app.formbricks.com/api/v2",
|
||||||
@@ -208,7 +208,7 @@ const v1ClientEndpoints = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
summary: "Create Display",
|
summary: "Create Display",
|
||||||
tags: ["Client API > Display"],
|
tags: ["Client API - Display"],
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: "https://app.formbricks.com/api/v2",
|
url: "https://app.formbricks.com/api/v2",
|
||||||
@@ -243,7 +243,7 @@ const v1ClientEndpoints = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
summary: "Get Environment State",
|
summary: "Get Environment State",
|
||||||
tags: ["Client API > Environment"],
|
tags: ["Client API - Environment"],
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: "https://app.formbricks.com/api/v2",
|
url: "https://app.formbricks.com/api/v2",
|
||||||
@@ -276,7 +276,7 @@ const v1ClientEndpoints = {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
summary: "Create or Identify User",
|
summary: "Create or Identify User",
|
||||||
tags: ["Client API > User"],
|
tags: ["Client API - User"],
|
||||||
servers: [
|
servers: [
|
||||||
{
|
{
|
||||||
url: "https://app.formbricks.com/api/v2",
|
url: "https://app.formbricks.com/api/v2",
|
||||||
@@ -291,7 +291,7 @@ const v1ClientEndpoints = {
|
|||||||
summary: "Upload Private File",
|
summary: "Upload Private File",
|
||||||
description:
|
description:
|
||||||
"API endpoint for uploading private files. Uploaded files are kept private so that only users with access to the specified environment can retrieve them. The endpoint validates the survey ID, file name, and file type from the request body, and returns a signed URL for S3 uploads along with a local upload URL.",
|
"API endpoint for uploading private files. Uploaded files are kept private so that only users with access to the specified environment can retrieve them. The endpoint validates the survey ID, file name, and file type from the request body, and returns a signed URL for S3 uploads along with a local upload URL.",
|
||||||
tags: ["Client API > File Upload"],
|
tags: ["Client API - File Upload"],
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
{
|
||||||
in: "path",
|
in: "path",
|
||||||
@@ -446,7 +446,7 @@ const v1ClientEndpoints = {
|
|||||||
summary: "Upload Private File to Local Storage",
|
summary: "Upload Private File to Local Storage",
|
||||||
description:
|
description:
|
||||||
'API endpoint for uploading private files to local storage. The request must include a valid signature, UUID, and timestamp to verify the upload. The file is provided as a Base64 encoded string in the request body. The "Content-Type" header must be set to a valid MIME type, and the file data must be a valid file object (buffer).',
|
'API endpoint for uploading private files to local storage. The request must include a valid signature, UUID, and timestamp to verify the upload. The file is provided as a Base64 encoded string in the request body. The "Content-Type" header must be set to a valid MIME type, and the file data must be a valid file object (buffer).',
|
||||||
tags: ["Client API > File Upload"],
|
tags: ["Client API - File Upload"],
|
||||||
parameters: [
|
parameters: [
|
||||||
{
|
{
|
||||||
in: "path",
|
in: "path",
|
||||||
@@ -472,7 +472,7 @@ const v1ClientEndpoints = {
|
|||||||
fileName: {
|
fileName: {
|
||||||
type: "string",
|
type: "string",
|
||||||
description:
|
description:
|
||||||
"This must be the `fileName` returned from the [Upload Private File](/api-v2-reference/client-api->-file-upload/upload-private-file) endpoint (Step 1).",
|
"This must be the `fileName` returned from the [Upload Private File](/api-v2-reference/client-api--file-upload/upload-private-file) endpoint (Step 1).",
|
||||||
},
|
},
|
||||||
fileType: {
|
fileType: {
|
||||||
type: "string",
|
type: "string",
|
||||||
|
|||||||
@@ -35,4 +35,4 @@ The documentation will be available at `http://localhost:3000`.
|
|||||||
|
|
||||||
- If Mintlify dev isn't running, try `mintlify install` to reinstall dependencies
|
- If Mintlify dev isn't running, try `mintlify install` to reinstall dependencies
|
||||||
- If a page loads as a 404, ensure you're in the `docs` folder with the `mint.json` file
|
- If a page loads as a 404, ensure you're in the `docs` folder with the `mint.json` file
|
||||||
- For other issues, please check our [Contributing Guidelines](./CONTRIBUTING.md)
|
- For other issues, please check our [Contributing Guidelines](https://github.com/formbricks/formbricks/blob/main/CONTRIBUTING.md)
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user