mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 10:19:51 -06:00
feat: auto subscribing to teams survey responses email (#1990)
Co-authored-by: Matthias Nannt <mail@matthiasnannt.com>
This commit is contained in:
committed by
GitHub
parent
6cea8a2246
commit
9761483530
@@ -2,8 +2,8 @@
|
||||
|
||||
import { getServerSession } from "next-auth";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { updateUser } from "@formbricks/lib/user/service";
|
||||
import { AuthorizationError } from "@formbricks/types/errors";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
@@ -13,9 +13,7 @@ export async function updateNotificationSettingsAction(notificationSettings: TUs
|
||||
throw new AuthorizationError("Not authenticated");
|
||||
}
|
||||
|
||||
// update user with notification settings
|
||||
await prisma.user.update({
|
||||
where: { id: session.user.id },
|
||||
data: { notificationSettings },
|
||||
await updateUser(session.user.id, {
|
||||
notificationSettings,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,27 +1,49 @@
|
||||
import { QuestionMarkCircleIcon, UsersIcon } from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@formbricks/ui/Tooltip";
|
||||
|
||||
import { Membership, User } from "../types";
|
||||
import { Membership } from "../types";
|
||||
import { NotificationSwitch } from "./NotificationSwitch";
|
||||
|
||||
interface EditAlertsProps {
|
||||
memberships: Membership[];
|
||||
user: User;
|
||||
user: TUser;
|
||||
environmentId: string;
|
||||
autoDisableNotificationType: string;
|
||||
autoDisableNotificationElementId: string;
|
||||
}
|
||||
|
||||
export default function EditAlerts({ memberships, user, environmentId }: EditAlertsProps) {
|
||||
export default function EditAlerts({
|
||||
memberships,
|
||||
user,
|
||||
environmentId,
|
||||
autoDisableNotificationType,
|
||||
autoDisableNotificationElementId,
|
||||
}: EditAlertsProps) {
|
||||
return (
|
||||
<>
|
||||
{memberships.map((membership) => (
|
||||
<>
|
||||
<div className="mb-5 flex items-center space-x-3 font-semibold">
|
||||
<div className="rounded-full bg-slate-100 p-1">
|
||||
<UsersIcon className="h-6 w-7 text-slate-600" />
|
||||
<div className="mb-5 grid grid-cols-6 items-center space-x-3">
|
||||
<div className="col-span-3 flex items-center space-x-3">
|
||||
<div className="rounded-full bg-slate-100 p-1">
|
||||
<UsersIcon className="h-6 w-7 text-slate-600" />
|
||||
</div>
|
||||
<p className="font-semibold text-slate-800">{membership.team.name}</p>
|
||||
</div>
|
||||
|
||||
<div className="col-span-3 flex items-center justify-end pr-2">
|
||||
<p className="pr-4 text-sm text-slate-600">Auto-subscribe to new surveys</p>
|
||||
<NotificationSwitch
|
||||
surveyOrProductOrTeamId={membership.team.id}
|
||||
notificationSettings={user.notificationSettings!}
|
||||
notificationType={"unsubscribedTeamIds"}
|
||||
autoDisableNotificationType={autoDisableNotificationType}
|
||||
autoDisableNotificationElementId={autoDisableNotificationElementId}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-slate-800">{membership.team.name}</p>
|
||||
</div>
|
||||
<div className="mb-6 rounded-lg border border-slate-200">
|
||||
<div className="grid h-12 grid-cols-3 content-center rounded-t-lg bg-slate-100 px-4 text-left text-sm font-semibold text-slate-900">
|
||||
@@ -57,9 +79,11 @@ export default function EditAlerts({ memberships, user, environmentId }: EditAle
|
||||
</div>
|
||||
<div className="col-span-1 text-center">
|
||||
<NotificationSwitch
|
||||
surveyOrProductId={survey.id}
|
||||
notificationSettings={user.notificationSettings}
|
||||
surveyOrProductOrTeamId={survey.id}
|
||||
notificationSettings={user.notificationSettings!}
|
||||
notificationType={"alert"}
|
||||
autoDisableNotificationType={autoDisableNotificationType}
|
||||
autoDisableNotificationElementId={autoDisableNotificationElementId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import { UsersIcon } from "@heroicons/react/24/solid";
|
||||
import Link from "next/link";
|
||||
|
||||
import { Membership, User } from "../types";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
import { Membership } from "../types";
|
||||
import { NotificationSwitch } from "./NotificationSwitch";
|
||||
|
||||
interface EditAlertsProps {
|
||||
memberships: Membership[];
|
||||
user: User;
|
||||
user: TUser;
|
||||
environmentId: string;
|
||||
}
|
||||
|
||||
@@ -34,8 +36,8 @@ export default function EditWeeklySummary({ memberships, user, environmentId }:
|
||||
<div className="col-span-2">{product?.name}</div>
|
||||
<div className="col-span-1 flex items-center justify-center">
|
||||
<NotificationSwitch
|
||||
surveyOrProductId={product.id}
|
||||
notificationSettings={user.notificationSettings}
|
||||
surveyOrProductOrTeamId={product.id}
|
||||
notificationSettings={user.notificationSettings!}
|
||||
notificationType={"weeklySummary"}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
@@ -10,35 +9,90 @@ import { Switch } from "@formbricks/ui/Switch";
|
||||
import { updateNotificationSettingsAction } from "../actions";
|
||||
|
||||
interface NotificationSwitchProps {
|
||||
surveyOrProductId: string;
|
||||
surveyOrProductOrTeamId: string;
|
||||
notificationSettings: TUserNotificationSettings;
|
||||
notificationType: "alert" | "weeklySummary";
|
||||
notificationType: "alert" | "weeklySummary" | "unsubscribedTeamIds";
|
||||
autoDisableNotificationType?: string;
|
||||
autoDisableNotificationElementId?: string;
|
||||
}
|
||||
|
||||
export function NotificationSwitch({
|
||||
surveyOrProductId,
|
||||
surveyOrProductOrTeamId,
|
||||
notificationSettings,
|
||||
notificationType,
|
||||
autoDisableNotificationType,
|
||||
autoDisableNotificationElementId,
|
||||
}: NotificationSwitchProps) {
|
||||
const router = useRouter();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const isChecked =
|
||||
notificationType === "unsubscribedTeamIds"
|
||||
? !notificationSettings.unsubscribedTeamIds?.includes(surveyOrProductOrTeamId)
|
||||
: notificationSettings[notificationType][surveyOrProductOrTeamId] === true;
|
||||
|
||||
const handleSwitchChange = async () => {
|
||||
setIsLoading(true);
|
||||
|
||||
let updatedNotificationSettings = { ...notificationSettings };
|
||||
if (notificationType === "unsubscribedTeamIds") {
|
||||
const unsubscribedTeamIds = updatedNotificationSettings.unsubscribedTeamIds ?? [];
|
||||
if (unsubscribedTeamIds.includes(surveyOrProductOrTeamId)) {
|
||||
updatedNotificationSettings.unsubscribedTeamIds = unsubscribedTeamIds.filter(
|
||||
(id) => id !== surveyOrProductOrTeamId
|
||||
);
|
||||
} else {
|
||||
updatedNotificationSettings.unsubscribedTeamIds = [...unsubscribedTeamIds, surveyOrProductOrTeamId];
|
||||
}
|
||||
} else {
|
||||
updatedNotificationSettings[notificationType][surveyOrProductOrTeamId] =
|
||||
!updatedNotificationSettings[notificationType][surveyOrProductOrTeamId];
|
||||
}
|
||||
|
||||
await updateNotificationSettingsAction(updatedNotificationSettings);
|
||||
setIsLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
autoDisableNotificationType &&
|
||||
autoDisableNotificationElementId === surveyOrProductOrTeamId &&
|
||||
isChecked
|
||||
) {
|
||||
switch (notificationType) {
|
||||
case "alert":
|
||||
if (notificationSettings[notificationType][surveyOrProductOrTeamId] === true) {
|
||||
handleSwitchChange();
|
||||
toast.success("You will not receive any more emails for responses on this survey!", {
|
||||
id: "notification-switch",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
case "unsubscribedTeamIds":
|
||||
if (!notificationSettings.unsubscribedTeamIds?.includes(surveyOrProductOrTeamId)) {
|
||||
handleSwitchChange();
|
||||
toast.success("You will not be auto-subscribed to this team's surveys anymore!", {
|
||||
id: "notification-switch",
|
||||
});
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Switch
|
||||
id="notification-switch"
|
||||
aria-label="toggle notification settings"
|
||||
checked={notificationSettings[notificationType][surveyOrProductId]}
|
||||
aria-label={`toggle notification settings for ${notificationType}`}
|
||||
checked={isChecked}
|
||||
disabled={isLoading}
|
||||
onCheckedChange={async () => {
|
||||
setIsLoading(true);
|
||||
// update notificiation settings
|
||||
const updatedNotificationSettings = { ...notificationSettings };
|
||||
updatedNotificationSettings[notificationType][surveyOrProductId] =
|
||||
!updatedNotificationSettings[notificationType][surveyOrProductId];
|
||||
await updateNotificationSettingsAction(notificationSettings);
|
||||
setIsLoading(false);
|
||||
toast.success(`Notification settings updated`, { id: "notification-switch" });
|
||||
router.refresh();
|
||||
await handleSwitchChange();
|
||||
toast.success("Notification settings updated", { id: "notification-switch" });
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -3,42 +3,24 @@ import { getServerSession } from "next-auth";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { authOptions } from "@formbricks/lib/authOptions";
|
||||
import { getUser } from "@formbricks/lib/user/service";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
import SettingsTitle from "../components/SettingsTitle";
|
||||
import EditAlerts from "./components/EditAlerts";
|
||||
import EditWeeklySummary from "./components/EditWeeklySummary";
|
||||
import IntegrationsTip from "./components/IntegrationsTip";
|
||||
import type { Membership, User } from "./types";
|
||||
import type { Membership } from "./types";
|
||||
|
||||
async function getUser(userId: string | undefined): Promise<User> {
|
||||
if (!userId) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
const userData = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: userId,
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
notificationSettings: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!userData) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
|
||||
const user = JSON.parse(JSON.stringify(userData)); // hack to remove the JsonValue type from the notificationSettings
|
||||
|
||||
return user;
|
||||
}
|
||||
|
||||
function cleanNotificationSettings(
|
||||
function setCompleteNotificationSettings(
|
||||
notificationSettings: TUserNotificationSettings,
|
||||
memberships: Membership[]
|
||||
) {
|
||||
const newNotificationSettings = { alert: {}, weeklySummary: {} };
|
||||
): TUserNotificationSettings {
|
||||
const newNotificationSettings = {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
unsubscribedTeamIds: notificationSettings.unsubscribedTeamIds || [],
|
||||
};
|
||||
for (const membership of memberships) {
|
||||
for (const product of membership.team.products) {
|
||||
// set default values for weekly summary
|
||||
@@ -95,13 +77,22 @@ async function getMemberships(userId: string): Promise<Membership[]> {
|
||||
return memberships;
|
||||
}
|
||||
|
||||
export default async function ProfileSettingsPage({ params }) {
|
||||
export default async function ProfileSettingsPage({ params, searchParams }) {
|
||||
const session = await getServerSession(authOptions);
|
||||
if (!session) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
const autoDisableNotificationType = searchParams["type"];
|
||||
const autoDisableNotificationElementId = searchParams["elementId"];
|
||||
|
||||
const [user, memberships] = await Promise.all([getUser(session.user.id), getMemberships(session.user.id)]);
|
||||
user.notificationSettings = cleanNotificationSettings(user.notificationSettings, memberships);
|
||||
if (!user) {
|
||||
throw new Error("User not found");
|
||||
}
|
||||
|
||||
if (user?.notificationSettings) {
|
||||
user.notificationSettings = setCompleteNotificationSettings(user.notificationSettings, memberships);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
@@ -109,7 +100,13 @@ export default async function ProfileSettingsPage({ params }) {
|
||||
<SettingsCard
|
||||
title="Email alerts (Surveys)"
|
||||
description="Set up an alert to get an email on new responses.">
|
||||
<EditAlerts memberships={memberships} user={user} environmentId={params.environmentId} />
|
||||
<EditAlerts
|
||||
memberships={memberships}
|
||||
user={user}
|
||||
environmentId={params.environmentId}
|
||||
autoDisableNotificationType={autoDisableNotificationType}
|
||||
autoDisableNotificationElementId={autoDisableNotificationElementId}
|
||||
/>
|
||||
</SettingsCard>
|
||||
<IntegrationsTip environmentId={params.environmentId} />
|
||||
<SettingsCard
|
||||
|
||||
@@ -7,6 +7,7 @@ import { prisma } from "@formbricks/database";
|
||||
import { INTERNAL_SECRET } from "@formbricks/lib/constants";
|
||||
import { sendResponseFinishedEmail } from "@formbricks/lib/emails/emails";
|
||||
import { getIntegrations } from "@formbricks/lib/integration/service";
|
||||
import { getResponseCountBySurveyId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey, updateSurvey } from "@formbricks/lib/survey/service";
|
||||
import { convertDatesInObject } from "@formbricks/lib/time";
|
||||
import { ZPipelineInput } from "@formbricks/types/pipelines";
|
||||
@@ -125,6 +126,9 @@ export async function POST(request: Request) {
|
||||
return false;
|
||||
});
|
||||
|
||||
// Exclude current response
|
||||
const responseCount = await getResponseCountBySurveyId(surveyId);
|
||||
|
||||
if (usersWithNotifications.length > 0) {
|
||||
// get survey
|
||||
if (!surveyData) {
|
||||
@@ -155,7 +159,7 @@ export async function POST(request: Request) {
|
||||
// send email to all users
|
||||
await Promise.all(
|
||||
usersWithNotifications.map(async (user) => {
|
||||
await sendResponseFinishedEmail(user.email, environmentId, survey, response);
|
||||
await sendResponseFinishedEmail(user.email, environmentId, survey, response, responseCount);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
@@ -21,9 +21,11 @@ export async function middleware(request: NextRequest) {
|
||||
const token = await getToken({ req: request });
|
||||
|
||||
if (isWebAppRoute(request.nextUrl.pathname) && !token) {
|
||||
return NextResponse.redirect(
|
||||
WEBAPP_URL + "/auth/login?callbackUrl=" + WEBAPP_URL + request.nextUrl.pathname
|
||||
const loginUrl = new URL(
|
||||
`/auth/login?callbackUrl=${encodeURIComponent(request.nextUrl.toString())}`,
|
||||
WEBAPP_URL
|
||||
);
|
||||
return NextResponse.redirect(loginUrl.href);
|
||||
}
|
||||
|
||||
const callbackUrl = request.nextUrl.searchParams.get("callbackUrl");
|
||||
@@ -73,5 +75,6 @@ export const config = {
|
||||
"/share/(.*)/:path",
|
||||
"/environments/:path*",
|
||||
"/api/auth/signout",
|
||||
"/auth/login",
|
||||
],
|
||||
};
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "../constants";
|
||||
import { createInviteToken, createToken, createTokenForLinkSurvey } from "../jwt";
|
||||
import { getQuestionResponseMapping } from "../responses";
|
||||
import { getTeamByEnvironmentId } from "../team/service";
|
||||
import { withEmailTemplate } from "./email-template";
|
||||
|
||||
const nodemailer = require("nodemailer");
|
||||
@@ -161,44 +162,52 @@ export const sendResponseFinishedEmail = async (
|
||||
email: string,
|
||||
environmentId: string,
|
||||
survey: { id: string; name: string; questions: TSurveyQuestion[] },
|
||||
response: TResponse
|
||||
response: TResponse,
|
||||
responseCount: number
|
||||
) => {
|
||||
const personEmail = response.person?.attributes["email"];
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
await sendEmail({
|
||||
to: email,
|
||||
subject: personEmail
|
||||
? `${personEmail} just completed your ${survey.name} survey ✅`
|
||||
: `A response for ${survey.name} was completed ✅`,
|
||||
replyTo: personEmail?.toString() || MAIL_FROM,
|
||||
html: withEmailTemplate(`<h1>Hey 👋</h1>Someone just completed your survey <strong>${
|
||||
survey.name
|
||||
}</strong><br/>
|
||||
html: withEmailTemplate(`
|
||||
<h1>Hey 👋</h1>
|
||||
<p>Congrats, you received a new response to your survey!
|
||||
Someone just completed your survey <strong>${survey.name}</strong><br/></p>
|
||||
|
||||
<hr/>
|
||||
<hr/>
|
||||
|
||||
${getQuestionResponseMapping(survey, response)
|
||||
.map(
|
||||
(question) =>
|
||||
question.answer &&
|
||||
`<div style="margin-top:1em;">
|
||||
<p style="margin:0px;">${question.question}</p>
|
||||
<p style="font-weight: 500; margin:0px; white-space:pre-wrap">${question.answer}</p>
|
||||
</div>`
|
||||
)
|
||||
.join("")}
|
||||
${getQuestionResponseMapping(survey, response)
|
||||
.map(
|
||||
(question) =>
|
||||
question.answer &&
|
||||
`<div style="margin-top:1em;">
|
||||
<p style="margin:0px;">${question.question}</p>
|
||||
<p style="font-weight: 500; margin:0px; white-space:pre-wrap">${question.answer}</p>
|
||||
</div>`
|
||||
)
|
||||
.join("")}
|
||||
|
||||
<a class="button" href="${WEBAPP_URL}/environments/${environmentId}/surveys/${
|
||||
survey.id
|
||||
}/responses?utm_source=email_notification&utm_medium=email&utm_content=view_responses_CTA">View all responses</a>
|
||||
<a class="button" href="${WEBAPP_URL}/environments/${environmentId}/surveys/${
|
||||
survey.id
|
||||
}/responses?utm_source=email_notification&utm_medium=email&utm_content=view_responses_CTA">${responseCount > 1 ? `View ${responseCount - 1} more ${responseCount === 2 ? "response" : "responses"}` : `View survey summary`}</a>
|
||||
|
||||
<div class="tooltip">
|
||||
<p class='brandcolor'><strong>Start a conversation 💡</strong></p>
|
||||
${
|
||||
personEmail
|
||||
? `<p>Hit 'Reply' or reach out manually: ${personEmail}</p>`
|
||||
: "<p>If you set the email address as an attribute in in-app surveys, you can reply directly to the respondent.</p>"
|
||||
}
|
||||
</div>
|
||||
<div class="tooltip">
|
||||
<p class='brandcolor'><strong>Start a conversation 💡</strong></p>
|
||||
${
|
||||
personEmail
|
||||
? `<p>Hit 'Reply' or reach out manually: ${personEmail}</p>`
|
||||
: "<p>If you set the email address as an attribute in in-app surveys, you can reply directly to the respondent.</p>"
|
||||
}
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<p><b>Don't want to get these emails?</b></p>
|
||||
<div style="margin-top:0.8em; background-color:#f1f5f9; border-radius:8px; padding:0.01em 1.6em; text-align:center; font-size:0.8em; line-height:1.2em;"><p><i>Turn off notifications for <a href="${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=alert&elementId=${survey.id}">this form</a>. <br/> Turn off notifications for <a href="${WEBAPP_URL}/environments/${environmentId}/settings/notifications?type=unsubscribedTeamIds&elementId=${team?.id}">all newly created forms</a>.</i></p></div>
|
||||
`),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -20,6 +20,7 @@ import { personCache } from "../person/cache";
|
||||
import { productCache } from "../product/cache";
|
||||
import { getProductByEnvironmentId } from "../product/service";
|
||||
import { responseCache } from "../response/cache";
|
||||
import { subscribeTeamMembersToSurveyResponses } from "../team/service";
|
||||
import { diffInDays, formatDateFields } from "../utils/datetime";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { surveyCache } from "./cache";
|
||||
@@ -528,6 +529,8 @@ export const createSurvey = async (
|
||||
triggers: survey.triggers.map((trigger) => trigger.actionClass.name),
|
||||
};
|
||||
|
||||
await subscribeTeamMembersToSurveyResponses(environmentId, survey.id);
|
||||
|
||||
surveyCache.revalidate({
|
||||
id: survey.id,
|
||||
environmentId: survey.environmentId,
|
||||
|
||||
@@ -12,6 +12,8 @@ import {
|
||||
TSurveyQuestionType,
|
||||
TSurveyWelcomeCard,
|
||||
} from "@formbricks/types/surveys";
|
||||
import { TTeam } from "@formbricks/types/teams";
|
||||
import { TUser } from "@formbricks/types/user";
|
||||
|
||||
import { selectSurvey } from "../service";
|
||||
|
||||
@@ -57,6 +59,38 @@ export const mockDisplay = {
|
||||
status: null,
|
||||
};
|
||||
|
||||
// id: true,
|
||||
// name: true,
|
||||
// email: true,
|
||||
// emailVerified: true,
|
||||
// imageUrl: true,
|
||||
// createdAt: true,
|
||||
// updatedAt: true,
|
||||
// onboardingCompleted: true,
|
||||
// twoFactorEnabled: true,
|
||||
// identityProvider: true,
|
||||
// objective: true,
|
||||
// notificationSettings: true,
|
||||
|
||||
export const mockUser: TUser = {
|
||||
id: mockId,
|
||||
name: "mock User",
|
||||
email: "test@unit.com",
|
||||
emailVerified: currentDate,
|
||||
imageUrl: "https://www.google.com",
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
onboardingCompleted: true,
|
||||
twoFactorEnabled: false,
|
||||
identityProvider: "google",
|
||||
objective: "improve_user_retention",
|
||||
notificationSettings: {
|
||||
alert: {},
|
||||
weeklySummary: {},
|
||||
unsubscribedTeamIds: [],
|
||||
},
|
||||
};
|
||||
|
||||
export const mockPerson: TPerson = {
|
||||
id: mockId,
|
||||
userId: mockId,
|
||||
@@ -127,6 +161,30 @@ const baseSurveyProperties = {
|
||||
...commonMockProperties,
|
||||
};
|
||||
|
||||
export const mockTeamOutput: TTeam = {
|
||||
id: mockId,
|
||||
name: "mock Team",
|
||||
createdAt: currentDate,
|
||||
updatedAt: currentDate,
|
||||
billing: {
|
||||
stripeCustomerId: null,
|
||||
features: {
|
||||
inAppSurvey: {
|
||||
status: "inactive",
|
||||
unlimited: false,
|
||||
},
|
||||
linkSurvey: {
|
||||
status: "inactive",
|
||||
unlimited: false,
|
||||
},
|
||||
userTargeting: {
|
||||
status: "inactive",
|
||||
unlimited: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
export const mockSurveyOutput: SurveyMock = {
|
||||
type: "web",
|
||||
status: "inProgress",
|
||||
|
||||
@@ -24,9 +24,11 @@ import {
|
||||
mockProduct,
|
||||
mockSurveyOutput,
|
||||
mockSurveyWithAttributesOutput,
|
||||
mockTeamOutput,
|
||||
mockTransformedSurveyOutput,
|
||||
mockTransformedSurveyWithAttributesIdOutput,
|
||||
mockTransformedSurveyWithAttributesOutput,
|
||||
mockUser,
|
||||
updateSurveyInput,
|
||||
} from "./survey.mock";
|
||||
|
||||
@@ -235,6 +237,27 @@ describe("Tests for createSurvey", () => {
|
||||
describe("Happy Path", () => {
|
||||
it("Creates a survey successfully", async () => {
|
||||
prismaMock.survey.create.mockResolvedValueOnce(mockSurveyWithAttributesOutput);
|
||||
prismaMock.team.findFirst.mockResolvedValueOnce(mockTeamOutput);
|
||||
prismaMock.user.findMany.mockResolvedValueOnce([
|
||||
{
|
||||
...mockUser,
|
||||
twoFactorSecret: null,
|
||||
backupCodes: null,
|
||||
password: null,
|
||||
identityProviderAccountId: null,
|
||||
groupId: null,
|
||||
role: "engineer",
|
||||
},
|
||||
]);
|
||||
prismaMock.user.update.mockResolvedValueOnce({
|
||||
...mockUser,
|
||||
twoFactorSecret: null,
|
||||
backupCodes: null,
|
||||
password: null,
|
||||
identityProviderAccountId: null,
|
||||
groupId: null,
|
||||
role: "engineer",
|
||||
});
|
||||
const createdSurvey = await createSurvey(mockId, createSurveyInput);
|
||||
expect(createdSurvey).toEqual(mockTransformedSurveyWithAttributesIdOutput);
|
||||
});
|
||||
|
||||
@@ -15,10 +15,12 @@ import {
|
||||
ZTeam,
|
||||
ZTeamCreateInput,
|
||||
} from "@formbricks/types/teams";
|
||||
import { TUserNotificationSettings } from "@formbricks/types/user";
|
||||
|
||||
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { environmentCache } from "../environment/cache";
|
||||
import { getProducts } from "../product/service";
|
||||
import { getUsersWithTeam, updateUser } from "../user/service";
|
||||
import { formatDateFields } from "../utils/datetime";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { teamCache } from "./cache";
|
||||
@@ -396,3 +398,38 @@ export const getTeamBillingInfo = async (teamId: string): Promise<TTeamBilling |
|
||||
tags: [teamCache.tag.byId(teamId)],
|
||||
}
|
||||
)();
|
||||
|
||||
export const subscribeTeamMembersToSurveyResponses = async (
|
||||
environmentId: string,
|
||||
surveyId: string
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
if (!team) {
|
||||
throw new ResourceNotFoundError("Team", environmentId);
|
||||
}
|
||||
|
||||
const users = await getUsersWithTeam(team.id);
|
||||
await Promise.all(
|
||||
users.map((user) => {
|
||||
if (!user.notificationSettings?.unsubscribedTeamIds?.includes(team?.id as string)) {
|
||||
const defaultSettings = { alert: {}, weeklySummary: {} };
|
||||
const updatedNotificationSettings: TUserNotificationSettings = {
|
||||
...defaultSettings,
|
||||
...user.notificationSettings,
|
||||
};
|
||||
|
||||
updatedNotificationSettings.alert[surveyId] = true;
|
||||
|
||||
return updateUser(user.id, {
|
||||
notificationSettings: updatedNotificationSettings,
|
||||
});
|
||||
}
|
||||
|
||||
return Promise.resolve();
|
||||
})
|
||||
);
|
||||
} catch (error) {
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -231,6 +231,23 @@ export const deleteUser = async (id: string): Promise<TUser> => {
|
||||
}
|
||||
};
|
||||
|
||||
export const getUsersWithTeam = async (teamId: string): Promise<TUser[]> => {
|
||||
validateInputs([teamId, ZId]);
|
||||
|
||||
const users = await prisma.user.findMany({
|
||||
where: {
|
||||
memberships: {
|
||||
some: {
|
||||
teamId,
|
||||
},
|
||||
},
|
||||
},
|
||||
select: responseSelection,
|
||||
});
|
||||
|
||||
return users;
|
||||
};
|
||||
|
||||
export const userIdRelatedToApiKey = async (apiKey: string) => {
|
||||
const userId = await prisma.apiKey.findUnique({
|
||||
where: { id: apiKey },
|
||||
|
||||
@@ -16,6 +16,7 @@ export type TUserObjective = z.infer<typeof ZUserObjective>;
|
||||
export const ZUserNotificationSettings = z.object({
|
||||
alert: z.record(z.boolean()),
|
||||
weeklySummary: z.record(z.boolean()),
|
||||
unsubscribedTeamIds: z.array(z.string()).optional(),
|
||||
});
|
||||
|
||||
export type TUserNotificationSettings = z.infer<typeof ZUserNotificationSettings>;
|
||||
|
||||
Reference in New Issue
Block a user