mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-30 02:10:12 -06:00
chore: make getSyncSurveys cached by default (#1609)
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
"use client";
|
||||
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { getPlacementStyle } from "@/app/lib/preview";
|
||||
import { cn } from "@formbricks/lib/cn";
|
||||
import { TPlacement } from "@formbricks/types/common";
|
||||
import { Label } from "@formbricks/ui/Label";
|
||||
import { RadioGroup, RadioGroupItem } from "@formbricks/ui/RadioGroup";
|
||||
const placements = [
|
||||
{ name: "Bottom Right", value: "bottomRight", disabled: false },
|
||||
{ name: "Top Right", value: "topRight", disabled: false },
|
||||
@@ -18,8 +18,8 @@ type TPlacementProps = {
|
||||
setCurrentPlacement: (placement: TPlacement) => void;
|
||||
setOverlay: (overlay: string) => void;
|
||||
overlay: string;
|
||||
setClickOutside: (clickOutside: boolean) => void;
|
||||
clickOutside: boolean;
|
||||
setClickOutsideClose: (clickOutside: boolean) => void;
|
||||
clickOutsideClose: boolean;
|
||||
};
|
||||
|
||||
export default function Placement({
|
||||
@@ -27,8 +27,8 @@ export default function Placement({
|
||||
currentPlacement,
|
||||
setOverlay,
|
||||
overlay,
|
||||
setClickOutside,
|
||||
clickOutside,
|
||||
setClickOutsideClose,
|
||||
clickOutsideClose,
|
||||
}: TPlacementProps) {
|
||||
return (
|
||||
<>
|
||||
@@ -78,8 +78,8 @@ export default function Placement({
|
||||
<div className="mt-6 space-y-2">
|
||||
<Label className="font-semibold">Allow users to exit by clicking outside the study</Label>
|
||||
<RadioGroup
|
||||
onValueChange={(value) => setClickOutside(value === "allow")}
|
||||
value={clickOutside ? "allow" : "disallow"}
|
||||
onValueChange={(value) => setClickOutsideClose(value === "allow")}
|
||||
value={clickOutsideClose ? "allow" : "disallow"}
|
||||
className="flex space-x-4">
|
||||
<div className="flex items-center space-x-2 whitespace-nowrap">
|
||||
<RadioGroupItem id="disallow" value="disallow" />
|
||||
|
||||
@@ -19,7 +19,8 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
const { type, productOverwrites } = localSurvey;
|
||||
const { brandColor, clickOutside, darkOverlay, placement, highlightBorderColor } = productOverwrites ?? {};
|
||||
const { brandColor, clickOutsideClose, darkOverlay, placement, highlightBorderColor } =
|
||||
productOverwrites ?? {};
|
||||
|
||||
const togglePlacement = () => {
|
||||
setLocalSurvey({
|
||||
@@ -93,12 +94,12 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
|
||||
});
|
||||
};
|
||||
|
||||
const handleClickOutside = (clickOutside: boolean) => {
|
||||
const handleClickOutsideClose = (clickOutsideClose: boolean) => {
|
||||
setLocalSurvey({
|
||||
...localSurvey,
|
||||
productOverwrites: {
|
||||
...localSurvey.productOverwrites,
|
||||
clickOutside,
|
||||
clickOutsideClose,
|
||||
},
|
||||
});
|
||||
};
|
||||
@@ -163,8 +164,8 @@ export default function StylingCard({ localSurvey, setLocalSurvey }: StylingCard
|
||||
setCurrentPlacement={handlePlacementChange}
|
||||
setOverlay={handleOverlay}
|
||||
overlay={darkOverlay ? "dark" : "light"}
|
||||
setClickOutside={handleClickOutside}
|
||||
clickOutside={!!clickOutside}
|
||||
setClickOutsideClose={handleClickOutsideClose}
|
||||
clickOutsideClose={!!clickOutsideClose}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
|
||||
import { personCache } from "@formbricks/lib/person/cache";
|
||||
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||
import { TJsState, ZJsPeopleAttributeInput } from "@formbricks/types/js";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
userId: string;
|
||||
environmentId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(req: Request, context: Context): Promise<NextResponse> {
|
||||
try {
|
||||
const { userId, environmentId } = context.params;
|
||||
const personId = userId; // legacy workaround for formbricks-js 1.2.0 & 1.2.1
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// validate using zod
|
||||
const inputValidation = ZJsPeopleAttributeInput.safeParse(jsonInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const { key, value } = inputValidation.data;
|
||||
|
||||
const person = await getPerson(personId);
|
||||
|
||||
if (!person) {
|
||||
return responses.notFoundResponse("Person", personId, true);
|
||||
}
|
||||
|
||||
let attributeClass = await getAttributeClassByName(environmentId, key);
|
||||
|
||||
// create new attribute class if not found
|
||||
if (attributeClass === null) {
|
||||
attributeClass = await createAttributeClass(environmentId, key, "code");
|
||||
}
|
||||
|
||||
if (!attributeClass) {
|
||||
return responses.internalServerErrorResponse("Unable to create attribute class", true);
|
||||
}
|
||||
|
||||
// upsert attribute (update or create)
|
||||
await updatePersonAttribute(personId, attributeClass.id, value);
|
||||
|
||||
personCache.revalidate({
|
||||
id: personId,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
surveyCache.revalidate({
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSyncSurveys(environmentId, person),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
// return state
|
||||
const state: TJsState = {
|
||||
person,
|
||||
surveys,
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
};
|
||||
|
||||
return responses.successResponse({ ...state }, true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(`Unable to complete request: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { markDisplayResponded } from "@formbricks/lib/display/service";
|
||||
import { markDisplayRespondedLegacy } from "@formbricks/lib/display/service";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
@@ -14,7 +14,7 @@ export async function POST(_: Request, { params }: { params: { displayId: string
|
||||
}
|
||||
|
||||
try {
|
||||
const display = await markDisplayResponded(displayId);
|
||||
const display = await markDisplayRespondedLegacy(displayId);
|
||||
return responses.successResponse(
|
||||
{
|
||||
...display,
|
||||
@@ -1,8 +1,8 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { updateDisplay } from "@formbricks/lib/display/service";
|
||||
import { TDisplayCreateInput, ZDisplayUpdateInput } from "@formbricks/types/displays";
|
||||
import { NextResponse } from "next/server";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { updateDisplayLegacy } from "@formbricks/lib/display/service";
|
||||
import { ZDisplayLegacyUpdateInput } from "@formbricks/types/displays";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
@@ -16,8 +16,8 @@ export async function PUT(
|
||||
if (!displayId) {
|
||||
return responses.badRequestResponse("Missing displayId", undefined, true);
|
||||
}
|
||||
const displayInput: TDisplayCreateInput = await request.json();
|
||||
const inputValidation = ZDisplayUpdateInput.safeParse(displayInput);
|
||||
const displayInput = await request.json();
|
||||
const inputValidation = ZDisplayLegacyUpdateInput.safeParse(displayInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
@@ -27,7 +27,7 @@ export async function PUT(
|
||||
);
|
||||
}
|
||||
try {
|
||||
const display = await updateDisplay(displayId, inputValidation.data);
|
||||
const display = await updateDisplayLegacy(displayId, inputValidation.data);
|
||||
return responses.successResponse(display, true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
@@ -1,11 +1,13 @@
|
||||
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
|
||||
import { personCache } from "@formbricks/lib/person/cache";
|
||||
import { getPerson, updatePersonAttribute } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { ZJsPeopleAttributeInput } from "@formbricks/types/js";
|
||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||
import { TJsState, ZJsPeopleAttributeInput } from "@formbricks/types/js";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
interface Context {
|
||||
@@ -37,9 +39,9 @@ export async function POST(req: Request, context: Context): Promise<NextResponse
|
||||
|
||||
const { key, value } = inputValidation.data;
|
||||
|
||||
const existingPerson = await getPerson(personId);
|
||||
const person = await getPerson(personId);
|
||||
|
||||
if (!existingPerson) {
|
||||
if (!person) {
|
||||
return responses.notFoundResponse("Person", personId, true);
|
||||
}
|
||||
|
||||
@@ -66,7 +68,23 @@ export async function POST(req: Request, context: Context): Promise<NextResponse
|
||||
environmentId,
|
||||
});
|
||||
|
||||
const state = await getUpdatedState(environmentId, personId);
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSyncSurveys(environmentId, person),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
// return state
|
||||
const state: TJsState = {
|
||||
person,
|
||||
surveys,
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
};
|
||||
|
||||
return responses.successResponse({ ...state }, true);
|
||||
} catch (error) {
|
||||
@@ -1,27 +1,27 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
||||
import { createResponseLegacy } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { createResponse } from "@formbricks/lib/response/service";
|
||||
import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponse, ZResponseLegacyInput } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { NextResponse } from "next/server";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<NextResponse> {
|
||||
const responseInput: TResponseInput = await request.json();
|
||||
const responseInput = await request.json();
|
||||
if (responseInput.personId === "legacy") {
|
||||
responseInput.personId = null;
|
||||
}
|
||||
const agent = UAParser(request.headers.get("user-agent"));
|
||||
const inputValidation = ZResponseInput.safeParse(responseInput);
|
||||
const inputValidation = ZResponseLegacyInput.safeParse(responseInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
@@ -67,7 +67,7 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
responseInput.personId = null;
|
||||
}
|
||||
|
||||
response = await createResponse({
|
||||
response = await createResponseLegacy({
|
||||
...responseInput,
|
||||
meta,
|
||||
});
|
||||
@@ -1,116 +0,0 @@
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { SERVICES_REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { displayCache } from "@formbricks/lib/display/cache";
|
||||
import { getDisplaysByPersonId } from "@formbricks/lib/display/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { productCache } from "@formbricks/lib/product/cache";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
// Helper function to calculate difference in days between two dates
|
||||
const diffInDays = (date1: Date, date2: Date) => {
|
||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
||||
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
};
|
||||
|
||||
export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
|
||||
unstable_cache(
|
||||
async () => {
|
||||
return await getSyncSurveys(environmentId, person);
|
||||
},
|
||||
[`getSyncSurveysCached-${environmentId}`],
|
||||
{
|
||||
tags: [
|
||||
displayCache.tag.byPersonId(person.id),
|
||||
surveyCache.tag.byEnvironmentId(environmentId),
|
||||
productCache.tag.byEnvironmentId(environmentId),
|
||||
],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
export const getSyncSurveys = async (
|
||||
environmentId: string,
|
||||
person: TPerson
|
||||
): Promise<TSurveyWithTriggers[]> => {
|
||||
// get recontactDays from product
|
||||
const product = await getProductByEnvironmentId(environmentId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
let surveys = await getSurveys(environmentId);
|
||||
|
||||
// filtered surveys for running and web
|
||||
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
|
||||
|
||||
const displays = await getDisplaysByPersonId(person.id);
|
||||
|
||||
// filter surveys that meet the displayOption criteria
|
||||
surveys = surveys.filter((survey) => {
|
||||
if (survey.displayOption === "respondMultiple") {
|
||||
return true;
|
||||
} else if (survey.displayOption === "displayOnce") {
|
||||
return displays.filter((display) => display.surveyId === survey.id).length === 0;
|
||||
} else if (survey.displayOption === "displayMultiple") {
|
||||
return (
|
||||
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null).length ===
|
||||
0
|
||||
);
|
||||
} else {
|
||||
throw Error("Invalid displayOption");
|
||||
}
|
||||
});
|
||||
|
||||
const attributeClasses = await getAttributeClasses(environmentId);
|
||||
|
||||
// filter surveys that meet the attributeFilters criteria
|
||||
const potentialSurveysWithAttributes = surveys.filter((survey) => {
|
||||
const attributeFilters = survey.attributeFilters;
|
||||
if (attributeFilters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// check if meets all attribute filters criterias
|
||||
return attributeFilters.every((attributeFilter) => {
|
||||
const attributeClassName = attributeClasses.find(
|
||||
(attributeClass) => attributeClass.id === attributeFilter.attributeClassId
|
||||
)?.name;
|
||||
if (!attributeClassName) {
|
||||
throw Error("Invalid attribute filter class");
|
||||
}
|
||||
const personAttributeValue = person.attributes[attributeClassName];
|
||||
if (attributeFilter.condition === "equals") {
|
||||
return personAttributeValue === attributeFilter.value;
|
||||
} else if (attributeFilter.condition === "notEquals") {
|
||||
return personAttributeValue !== attributeFilter.value;
|
||||
} else {
|
||||
throw Error("Invalid attribute filter condition");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const latestDisplay = displays[0];
|
||||
|
||||
// filter surveys that meet the recontactDays criteria
|
||||
surveys = potentialSurveysWithAttributes.filter((survey) => {
|
||||
if (!latestDisplay) {
|
||||
return true;
|
||||
} else if (survey.recontactDays !== null) {
|
||||
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
|
||||
if (!lastDisplaySurvey) {
|
||||
return true;
|
||||
}
|
||||
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
|
||||
} else if (product.recontactDays !== null) {
|
||||
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return surveys;
|
||||
};
|
||||
@@ -1,133 +0,0 @@
|
||||
import { getSyncSurveysCached } from "@/app/api/v1/(legacy)/js/lib/surveys";
|
||||
import { IS_FORMBRICKS_CLOUD, MAU_LIMIT, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { captureTelemetry } from "@formbricks/lib/telemetry";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TJsLegacyState } from "@formbricks/types/js";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
|
||||
const captureNewSessionTelemetry = async (jsVersion?: string): Promise<void> => {
|
||||
await captureTelemetry("state update", { jsVersion: jsVersion ?? "unknown" });
|
||||
};
|
||||
|
||||
export const getUpdatedState = async (
|
||||
environmentId: string,
|
||||
personId: string,
|
||||
jsVersion?: string
|
||||
): Promise<TJsLegacyState> => {
|
||||
let environment: TEnvironment | null;
|
||||
|
||||
if (jsVersion) {
|
||||
captureNewSessionTelemetry(jsVersion);
|
||||
}
|
||||
|
||||
// check if environment exists
|
||||
environment = await getEnvironment(environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment does not exist");
|
||||
}
|
||||
|
||||
if (!environment?.widgetSetupCompleted) {
|
||||
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
|
||||
}
|
||||
|
||||
// check team subscriptons
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team does not exist");
|
||||
}
|
||||
|
||||
// check if Monthly Active Users limit is reached
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
const hasUserTargetingSubscription =
|
||||
team?.billing?.features.userTargeting.status &&
|
||||
team?.billing?.features.userTargeting.status in ["active", "canceled"];
|
||||
const currentMau = await getMonthlyActiveTeamPeopleCount(team.id);
|
||||
const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
|
||||
|
||||
if (isMauLimitReached) {
|
||||
const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
|
||||
throw new Error(errorMessage);
|
||||
|
||||
// if (!personId) {
|
||||
// // don't allow new people
|
||||
// throw new Error(errorMessage);
|
||||
// }
|
||||
// const session = await getSession(sessionId);
|
||||
// if (!session) {
|
||||
// // don't allow new sessions
|
||||
// throw new Error(errorMessage);
|
||||
// }
|
||||
// // check if session was created this month (user already active this month)
|
||||
// const now = new Date();
|
||||
// const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
// if (new Date(session.createdAt) < firstDayOfMonth) {
|
||||
// throw new Error(errorMessage);
|
||||
// }
|
||||
}
|
||||
}
|
||||
|
||||
const person = await getPerson(personId);
|
||||
|
||||
if (!person) {
|
||||
throw new Error("Person not found");
|
||||
}
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSyncSurveysCached(environmentId, person),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
// return state
|
||||
const state: TJsLegacyState = {
|
||||
person: person!,
|
||||
session: {},
|
||||
surveys,
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
};
|
||||
|
||||
return state;
|
||||
};
|
||||
|
||||
export const getPublicUpdatedState = async (environmentId: string) => {
|
||||
// check if environment exists
|
||||
const environment = await getEnvironment(environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment does not exist");
|
||||
}
|
||||
|
||||
// TODO: check if Monthly Active Users limit is reached
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSurveys(environmentId),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
const state: TJsLegacyState = {
|
||||
surveys,
|
||||
session: {},
|
||||
noCodeActionClasses: noCodeActionClasses.filter((actionClass) => actionClass.type === "noCode"),
|
||||
product,
|
||||
person: null,
|
||||
};
|
||||
|
||||
return state;
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
|
||||
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/sync/lib/sync";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { createAttributeClass, getAttributeClassByName } from "@formbricks/lib/attributeClass/service";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/lib/sync";
|
||||
import { getUpdatedState } from "@/app/api/v1/(legacy)/js/sync/lib/sync";
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getOrCreatePersonByUserId } from "@formbricks/lib/person/service";
|
||||
@@ -29,7 +29,6 @@ export async function POST(req: Request): Promise<NextResponse> {
|
||||
const personWithUserId = await getOrCreatePersonByUserId(userId, environmentId);
|
||||
|
||||
const state = await getUpdatedState(environmentId, personWithUserId.id);
|
||||
|
||||
return responses.successResponse({ ...state }, true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
11
apps/web/app/api/v1/(legacy)/js/sync/lib/legacy.ts
Normal file
11
apps/web/app/api/v1/(legacy)/js/sync/lib/legacy.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
import { TJsLegacyState, TJsState } from "@formbricks/types/js";
|
||||
|
||||
export const transformLegacySurveys = (state: TJsState): TJsLegacyState => {
|
||||
const updatedState: any = { ...state };
|
||||
updatedState.surveys = updatedState.surveys.map((survey) => {
|
||||
const updatedSurvey = { ...survey };
|
||||
updatedSurvey.triggers = updatedSurvey.triggers.map((trigger) => ({ name: trigger }));
|
||||
return updatedSurvey;
|
||||
});
|
||||
return { ...updatedState, session: {} };
|
||||
};
|
||||
@@ -1,116 +0,0 @@
|
||||
import { getAttributeClasses } from "@formbricks/lib/attributeClass/service";
|
||||
import { SERVICES_REVALIDATION_INTERVAL } from "@formbricks/lib/constants";
|
||||
import { displayCache } from "@formbricks/lib/display/cache";
|
||||
import { getDisplaysByPersonId } from "@formbricks/lib/display/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { productCache } from "@formbricks/lib/product/cache";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { surveyCache } from "@formbricks/lib/survey/cache";
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { unstable_cache } from "next/cache";
|
||||
|
||||
// Helper function to calculate difference in days between two dates
|
||||
const diffInDays = (date1: Date, date2: Date) => {
|
||||
const diffTime = Math.abs(date2.getTime() - date1.getTime());
|
||||
return Math.floor(diffTime / (1000 * 60 * 60 * 24));
|
||||
};
|
||||
|
||||
export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
|
||||
unstable_cache(
|
||||
async () => {
|
||||
return await getSyncSurveys(environmentId, person);
|
||||
},
|
||||
[`getSyncSurveysCached-${environmentId}-${person.id}`],
|
||||
{
|
||||
tags: [
|
||||
displayCache.tag.byPersonId(person.id),
|
||||
surveyCache.tag.byEnvironmentId(environmentId),
|
||||
productCache.tag.byEnvironmentId(environmentId),
|
||||
],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
export const getSyncSurveys = async (
|
||||
environmentId: string,
|
||||
person: TPerson
|
||||
): Promise<TSurveyWithTriggers[]> => {
|
||||
// get recontactDays from product
|
||||
const product = await getProductByEnvironmentId(environmentId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
let surveys = await getSurveys(environmentId);
|
||||
|
||||
// filtered surveys for running and web
|
||||
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
|
||||
|
||||
const displays = await getDisplaysByPersonId(person.id);
|
||||
|
||||
// filter surveys that meet the displayOption criteria
|
||||
surveys = surveys.filter((survey) => {
|
||||
if (survey.displayOption === "respondMultiple") {
|
||||
return true;
|
||||
} else if (survey.displayOption === "displayOnce") {
|
||||
return displays.filter((display) => display.surveyId === survey.id).length === 0;
|
||||
} else if (survey.displayOption === "displayMultiple") {
|
||||
return (
|
||||
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null).length ===
|
||||
0
|
||||
);
|
||||
} else {
|
||||
throw Error("Invalid displayOption");
|
||||
}
|
||||
});
|
||||
|
||||
const attributeClasses = await getAttributeClasses(environmentId);
|
||||
|
||||
// filter surveys that meet the attributeFilters criteria
|
||||
const potentialSurveysWithAttributes = surveys.filter((survey) => {
|
||||
const attributeFilters = survey.attributeFilters;
|
||||
if (attributeFilters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// check if meets all attribute filters criterias
|
||||
return attributeFilters.every((attributeFilter) => {
|
||||
const attributeClassName = attributeClasses.find(
|
||||
(attributeClass) => attributeClass.id === attributeFilter.attributeClassId
|
||||
)?.name;
|
||||
if (!attributeClassName) {
|
||||
throw Error("Invalid attribute filter class");
|
||||
}
|
||||
const personAttributeValue = person.attributes[attributeClassName];
|
||||
if (attributeFilter.condition === "equals") {
|
||||
return personAttributeValue === attributeFilter.value;
|
||||
} else if (attributeFilter.condition === "notEquals") {
|
||||
return personAttributeValue !== attributeFilter.value;
|
||||
} else {
|
||||
throw Error("Invalid attribute filter condition");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const latestDisplay = displays[0];
|
||||
|
||||
// filter surveys that meet the recontactDays criteria
|
||||
surveys = potentialSurveysWithAttributes.filter((survey) => {
|
||||
if (!latestDisplay) {
|
||||
return true;
|
||||
} else if (survey.recontactDays !== null) {
|
||||
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
|
||||
if (!lastDisplaySurvey) {
|
||||
return true;
|
||||
}
|
||||
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
|
||||
} else if (product.recontactDays !== null) {
|
||||
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return surveys;
|
||||
};
|
||||
@@ -1,4 +1,3 @@
|
||||
import { getSyncSurveysCached } from "@/app/api/v1/(legacy)/js/sync/lib/surveys";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import {
|
||||
IS_FORMBRICKS_CLOUD,
|
||||
@@ -9,15 +8,25 @@ import {
|
||||
import { getEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getSurveys, getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||
import {
|
||||
getMonthlyActiveTeamPeopleCount,
|
||||
getMonthlyTeamResponseCount,
|
||||
getTeamByEnvironmentId,
|
||||
} from "@formbricks/lib/team/service";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TJsLegacyState } from "@formbricks/types/js";
|
||||
import { TJsLegacyState, TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export const transformLegacySurveys = (surveys: TSurvey[]): TSurveyWithTriggers[] => {
|
||||
const updatedSurveys = surveys.map((survey) => {
|
||||
const updatedSurvey: any = { ...survey };
|
||||
updatedSurvey.triggers = updatedSurvey.triggers.map((trigger) => ({ name: trigger }));
|
||||
return updatedSurvey;
|
||||
});
|
||||
return updatedSurveys;
|
||||
};
|
||||
|
||||
export const getUpdatedState = async (environmentId: string, personId?: string): Promise<TJsLegacyState> => {
|
||||
let environment: TEnvironment | null;
|
||||
@@ -85,12 +94,14 @@ export const getUpdatedState = async (environmentId: string, personId?: string):
|
||||
if (isAppSurveyLimitReached) {
|
||||
surveys = [];
|
||||
} else if (isPerson) {
|
||||
surveys = await getSyncSurveysCached(environmentId, person as TPerson);
|
||||
surveys = await getSyncSurveys(environmentId, person as TPerson);
|
||||
} else {
|
||||
surveys = await getSurveys(environmentId);
|
||||
surveys = surveys.filter((survey) => survey.type === "web");
|
||||
surveys = surveys.filter((survey) => survey.type === "web" && survey.status === "inProgress");
|
||||
}
|
||||
|
||||
surveys = transformLegacySurveys(surveys);
|
||||
|
||||
// get/create rest of the state
|
||||
const [noCodeActionClasses, product] = await Promise.all([
|
||||
getActionClasses(environmentId),
|
||||
|
||||
@@ -16,10 +16,11 @@ export async function OPTIONS(): Promise<NextResponse> {
|
||||
}
|
||||
|
||||
export async function PUT(request: Request, context: Context): Promise<NextResponse> {
|
||||
const { displayId } = context.params;
|
||||
const { displayId, environmentId } = context.params;
|
||||
const jsonInput = await request.json();
|
||||
const inputValidation = ZDisplayUpdateInput.safeParse({
|
||||
...jsonInput,
|
||||
environmentId,
|
||||
});
|
||||
|
||||
if (!inputValidation.success) {
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getLatestActionByPersonId } from "@formbricks/lib/action/service";
|
||||
import { getActionClasses } from "@formbricks/lib/actionClass/service";
|
||||
import { IS_FORMBRICKS_CLOUD, MAU_LIMIT, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
|
||||
import { IS_FORMBRICKS_CLOUD, PRICING_USERTARGETING_FREE_MTU } from "@formbricks/lib/constants";
|
||||
import { getEnvironment, updateEnvironment } from "@formbricks/lib/environment/service";
|
||||
import { getOrCreatePersonByUserId } from "@formbricks/lib/person/service";
|
||||
import { getOrCreatePersonByUserId, getPersonByUserId } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSyncSurveysCached } from "@formbricks/lib/survey/service";
|
||||
import { getSyncSurveys } from "@formbricks/lib/survey/service";
|
||||
import { getMonthlyActiveTeamPeopleCount, getTeamByEnvironmentId } from "@formbricks/lib/team/service";
|
||||
import { TEnvironment } from "@formbricks/types/environment";
|
||||
import { TJsState, ZJsPeopleUserIdInput } from "@formbricks/types/js";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
@@ -43,52 +45,52 @@ export async function GET(
|
||||
|
||||
const { environmentId, userId } = inputValidation.data;
|
||||
|
||||
// check if person exists
|
||||
const person = await getOrCreatePersonByUserId(userId, environmentId);
|
||||
|
||||
if (!person) {
|
||||
return responses.badRequestResponse(`Person with userId ${userId} not found`);
|
||||
}
|
||||
|
||||
let environment: TEnvironment | null;
|
||||
|
||||
// check if environment exists
|
||||
environment = await getEnvironment(environmentId);
|
||||
|
||||
if (!environment) {
|
||||
throw new Error("Environment does not exist");
|
||||
}
|
||||
|
||||
if (!environment?.widgetSetupCompleted) {
|
||||
await updateEnvironment(environment.id, { widgetSetupCompleted: true });
|
||||
}
|
||||
|
||||
// check team subscriptons
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team does not exist");
|
||||
}
|
||||
|
||||
// check if Monthly Active Users limit is reached
|
||||
// check if MAU limit is reached
|
||||
let isMauLimitReached = false;
|
||||
if (IS_FORMBRICKS_CLOUD) {
|
||||
// check team subscriptons
|
||||
const team = await getTeamByEnvironmentId(environmentId);
|
||||
|
||||
if (!team) {
|
||||
throw new Error("Team does not exist");
|
||||
}
|
||||
const hasUserTargetingSubscription =
|
||||
team?.billing?.features.userTargeting.status &&
|
||||
team?.billing?.features.userTargeting.status in ["active", "canceled"];
|
||||
const currentMau = await getMonthlyActiveTeamPeopleCount(team.id);
|
||||
const isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
|
||||
isMauLimitReached = !hasUserTargetingSubscription && currentMau >= PRICING_USERTARGETING_FREE_MTU;
|
||||
}
|
||||
|
||||
// TODO: Problem is that if isMauLimitReached, all sync request will fail
|
||||
// But what we essentially want, is to fail only for new people syncing for the first time
|
||||
|
||||
if (isMauLimitReached) {
|
||||
const errorMessage = `Monthly Active Users limit reached in ${environmentId} (${currentMau}/${MAU_LIMIT})`;
|
||||
let person: TPerson | null;
|
||||
if (!isMauLimitReached) {
|
||||
person = await getOrCreatePersonByUserId(userId, environmentId);
|
||||
} else {
|
||||
person = await getPersonByUserId(userId, environmentId);
|
||||
const errorMessage = `Monthly Active Users limit in the current plan is reached in ${environmentId}`;
|
||||
if (!person) {
|
||||
throw new Error(errorMessage);
|
||||
} else {
|
||||
// check if person has been active this month
|
||||
const latestAction = await getLatestActionByPersonId(person.id);
|
||||
if (!latestAction || new Date(latestAction.createdAt).getMonth() !== new Date().getMonth()) {
|
||||
throw new Error(errorMessage);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const [surveys, noCodeActionClasses, product] = await Promise.all([
|
||||
getSyncSurveysCached(environmentId, person),
|
||||
getSyncSurveys(environmentId, person),
|
||||
getActionClasses(environmentId),
|
||||
getProductByEnvironmentId(environmentId),
|
||||
]);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { getOrCreatePersonByUserId, updatePerson } from "@formbricks/lib/person/service";
|
||||
import { ZPersonUpdateInput } from "@formbricks/types/people";
|
||||
import { NextResponse } from "next/server";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
userId: string;
|
||||
environmentId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(req: Request, context: Context): Promise<NextResponse> {
|
||||
try {
|
||||
const { userId, environmentId } = context.params;
|
||||
const jsonInput = await req.json();
|
||||
|
||||
// validate using zod
|
||||
const inputValidation = ZPersonUpdateInput.safeParse(jsonInput);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(inputValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const person = await getOrCreatePersonByUserId(userId, environmentId);
|
||||
|
||||
if (!person) {
|
||||
return responses.notFoundResponse("PersonByUserId", userId, true);
|
||||
}
|
||||
|
||||
const updatedPerson = await updatePerson(person.id, inputValidation.data);
|
||||
|
||||
return responses.successResponse(updatedPerson, true);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(`Unable to complete request: ${error.message}`, true);
|
||||
}
|
||||
}
|
||||
@@ -6,6 +6,7 @@ import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { updateResponse } from "@formbricks/lib/response/service";
|
||||
import { ZResponseUpdateInput } from "@formbricks/types/responses";
|
||||
import { NextResponse } from "next/server";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
@@ -23,6 +24,13 @@ export async function PUT(
|
||||
|
||||
const responseUpdate = await request.json();
|
||||
|
||||
// legacy workaround for formbricks-js 1.2.0 & 1.2.1
|
||||
if (responseUpdate.personId && typeof responseUpdate.personId === "string") {
|
||||
const person = await getPerson(responseUpdate.personId);
|
||||
responseUpdate.userId = person?.userId;
|
||||
delete responseUpdate.personId;
|
||||
}
|
||||
|
||||
const inputValidation = ZResponseUpdateInput.safeParse(responseUpdate);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
|
||||
@@ -1,23 +1,50 @@
|
||||
import { responses } from "@/app/lib/api/response";
|
||||
import { transformErrorToDetails } from "@/app/lib/api/validator";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { getPerson } from "@formbricks/lib/person/service";
|
||||
import { capturePosthogEvent } from "@formbricks/lib/posthogServer";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { createResponse } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getTeamDetails } from "@formbricks/lib/teamDetail/service";
|
||||
import { TResponse, TResponseInput, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { InvalidInputError } from "@formbricks/types/errors";
|
||||
import { TResponse, ZResponseInput } from "@formbricks/types/responses";
|
||||
import { NextResponse } from "next/server";
|
||||
import { UAParser } from "ua-parser-js";
|
||||
|
||||
interface Context {
|
||||
params: {
|
||||
environmentId: string;
|
||||
};
|
||||
}
|
||||
|
||||
export async function OPTIONS(): Promise<NextResponse> {
|
||||
return responses.successResponse({}, true);
|
||||
}
|
||||
|
||||
export async function POST(request: Request): Promise<NextResponse> {
|
||||
const responseInput: TResponseInput = await request.json();
|
||||
export async function POST(request: Request, context: Context): Promise<NextResponse> {
|
||||
const { environmentId } = context.params;
|
||||
const environmentIdValidation = ZId.safeParse(environmentId);
|
||||
|
||||
if (!environmentIdValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
"Fields are missing or incorrectly formatted",
|
||||
transformErrorToDetails(environmentIdValidation.error),
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const responseInput = await request.json();
|
||||
|
||||
// legacy workaround for formbricks-js 1.2.0 & 1.2.1
|
||||
if (responseInput.personId && typeof responseInput.personId === "string") {
|
||||
const person = await getPerson(responseInput.personId);
|
||||
responseInput.userId = person?.userId;
|
||||
delete responseInput.personId;
|
||||
}
|
||||
|
||||
const agent = UAParser(request.headers.get("user-agent"));
|
||||
const inputValidation = ZResponseInput.safeParse(responseInput);
|
||||
const inputValidation = ZResponseInput.safeParse({ ...responseInput, environmentId });
|
||||
|
||||
if (!inputValidation.success) {
|
||||
return responses.badRequestResponse(
|
||||
@@ -27,17 +54,20 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
);
|
||||
}
|
||||
|
||||
let survey;
|
||||
|
||||
try {
|
||||
survey = await getSurvey(responseInput.surveyId);
|
||||
} catch (error) {
|
||||
if (error instanceof InvalidInputError) {
|
||||
return responses.badRequestResponse(error.message);
|
||||
} else {
|
||||
console.error(error);
|
||||
return responses.internalServerErrorResponse(error.message);
|
||||
}
|
||||
// get and check survey
|
||||
const survey = await getSurvey(responseInput.surveyId);
|
||||
if (!survey) {
|
||||
return responses.notFoundResponse("Survey", responseInput.surveyId, true);
|
||||
}
|
||||
if (survey.environmentId !== environmentId) {
|
||||
return responses.badRequestResponse(
|
||||
"Survey is part of another environment",
|
||||
{
|
||||
"survey.environmentId": survey.environmentId,
|
||||
environmentId,
|
||||
},
|
||||
true
|
||||
);
|
||||
}
|
||||
|
||||
const teamDetails = await getTeamDetails(survey.environmentId);
|
||||
@@ -54,14 +84,8 @@ export async function POST(request: Request): Promise<NextResponse> {
|
||||
},
|
||||
};
|
||||
|
||||
// check if personId is anonymous
|
||||
if (responseInput.personId === "anonymous") {
|
||||
// remove this from the request
|
||||
responseInput.personId = null;
|
||||
}
|
||||
|
||||
response = await createResponse({
|
||||
...responseInput,
|
||||
...inputValidation.data,
|
||||
meta,
|
||||
});
|
||||
} catch (error) {
|
||||
|
||||
@@ -10,11 +10,11 @@ export const createResponse = async (
|
||||
finished: boolean = false
|
||||
): Promise<any> => {
|
||||
const api = formbricks.getApi();
|
||||
const personId = formbricks.getPerson()?.id;
|
||||
const userId = formbricks.getPerson()?.userId;
|
||||
|
||||
return await api.client.response.create({
|
||||
surveyId,
|
||||
personId: personId ?? "",
|
||||
userId: userId ?? "",
|
||||
finished,
|
||||
data,
|
||||
});
|
||||
|
||||
@@ -18,7 +18,7 @@ import { FormbricksAPI } from "@formbricks/api";
|
||||
interface LinkSurveyProps {
|
||||
survey: TSurvey;
|
||||
product: TProduct;
|
||||
personId?: string;
|
||||
userId?: string;
|
||||
emailVerificationStatus?: string;
|
||||
prefillAnswer?: string;
|
||||
singleUseId?: string;
|
||||
@@ -29,7 +29,7 @@ interface LinkSurveyProps {
|
||||
export default function LinkSurvey({
|
||||
survey,
|
||||
product,
|
||||
personId,
|
||||
userId,
|
||||
emailVerificationStatus,
|
||||
prefillAnswer,
|
||||
singleUseId,
|
||||
@@ -41,9 +41,7 @@ export default function LinkSurvey({
|
||||
const isPreview = searchParams?.get("preview") === "true";
|
||||
const sourceParam = searchParams?.get("source");
|
||||
// pass in the responseId if the survey is a single use survey, ensures survey state is updated with the responseId
|
||||
const [surveyState, setSurveyState] = useState(
|
||||
new SurveyState(survey.id, singleUseId, responseId, personId)
|
||||
);
|
||||
const [surveyState, setSurveyState] = useState(new SurveyState(survey.id, singleUseId, responseId, userId));
|
||||
const [activeQuestionId, setActiveQuestionId] = useState<string>(
|
||||
survey.welcomeCard.enabled ? "start" : survey?.questions[0]?.id
|
||||
);
|
||||
|
||||
@@ -14,7 +14,7 @@ import { cn } from "@formbricks/lib/cn";
|
||||
interface LinkSurveyPinScreenProps {
|
||||
surveyId: string;
|
||||
product: TProduct;
|
||||
personId?: string;
|
||||
userId?: string;
|
||||
emailVerificationStatus?: string;
|
||||
prefillAnswer?: string;
|
||||
singleUseId?: string;
|
||||
@@ -28,7 +28,7 @@ const LinkSurveyPinScreen: NextPage<LinkSurveyPinScreenProps> = (props) => {
|
||||
product,
|
||||
webAppUrl,
|
||||
emailVerificationStatus,
|
||||
personId,
|
||||
userId,
|
||||
prefillAnswer,
|
||||
singleUseId,
|
||||
singleUseResponse,
|
||||
@@ -103,7 +103,7 @@ const LinkSurveyPinScreen: NextPage<LinkSurveyPinScreenProps> = (props) => {
|
||||
<LinkSurvey
|
||||
survey={survey}
|
||||
product={product}
|
||||
personId={personId}
|
||||
userId={userId}
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
prefillAnswer={prefillAnswer}
|
||||
singleUseId={singleUseId}
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
export const revalidate = REVALIDATION_INTERVAL;
|
||||
|
||||
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
||||
import LinkSurvey from "@/app/s/[surveyId]/components/LinkSurvey";
|
||||
import PinScreen from "@/app/s/[surveyId]/components/PinScreen";
|
||||
import SurveyInactive from "@/app/s/[surveyId]/components/SurveyInactive";
|
||||
import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
|
||||
import { REVALIDATION_INTERVAL, WEBAPP_URL } from "@formbricks/lib/constants";
|
||||
import { getOrCreatePersonByUserId } from "@formbricks/lib/person/service";
|
||||
import { getProductByEnvironmentId } from "@formbricks/lib/product/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { getEmailVerificationStatus } from "./lib/helpers";
|
||||
import { checkValidity } from "@/app/s/[surveyId]/lib/prefilling";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getResponseBySingleUseId } from "@formbricks/lib/response/service";
|
||||
import { getSurvey } from "@formbricks/lib/survey/service";
|
||||
import { TResponse } from "@formbricks/types/responses";
|
||||
import { validateSurveySingleUseId } from "@/app/lib/singleUseSurveys";
|
||||
import type { Metadata } from "next";
|
||||
import PinScreen from "@/app/s/[surveyId]/components/PinScreen";
|
||||
import { notFound } from "next/navigation";
|
||||
import { getEmailVerificationStatus } from "./lib/helpers";
|
||||
|
||||
interface LinkSurveyPageProps {
|
||||
params: {
|
||||
@@ -146,9 +146,8 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
|
||||
}
|
||||
|
||||
const userId = searchParams.userId;
|
||||
let person;
|
||||
if (userId) {
|
||||
person = await getOrCreatePersonByUserId(userId, survey.environmentId);
|
||||
await getOrCreatePersonByUserId(userId, survey.environmentId);
|
||||
}
|
||||
|
||||
const isSurveyPinProtected = Boolean(!!survey && survey.pin);
|
||||
@@ -158,7 +157,7 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
|
||||
<PinScreen
|
||||
surveyId={survey.id}
|
||||
product={product}
|
||||
personId={person?.id}
|
||||
userId={userId}
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
|
||||
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
|
||||
@@ -172,7 +171,7 @@ export default async function LinkSurveyPage({ params, searchParams }: LinkSurve
|
||||
<LinkSurvey
|
||||
survey={survey}
|
||||
product={product}
|
||||
personId={person?.id}
|
||||
userId={userId}
|
||||
emailVerificationStatus={emailVerificationStatus}
|
||||
prefillAnswer={isPrefilledAnswerValid ? prefillAnswer : null}
|
||||
singleUseId={isSingleUseSurvey ? singleUseId : undefined}
|
||||
|
||||
@@ -20,7 +20,7 @@ export class DisplayAPI {
|
||||
|
||||
async update(
|
||||
displayId: string,
|
||||
displayInput: TDisplayUpdateInput
|
||||
displayInput: Omit<TDisplayUpdateInput, "environmentId">
|
||||
): Promise<Result<TDisplay, NetworkError | Error>> {
|
||||
return makeRequest(
|
||||
this.apiHost,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { makeRequest } from "../../utils/makeRequest";
|
||||
import { NetworkError } from "@formbricks/types/errors";
|
||||
import { Result } from "@formbricks/types/errorHandlers";
|
||||
import { NetworkError } from "@formbricks/types/errors";
|
||||
import { TResponse, TResponseInput, TResponseUpdateInput } from "@formbricks/types/responses";
|
||||
import { makeRequest } from "../../utils/makeRequest";
|
||||
|
||||
type TResponseUpdateInputWithResponseId = TResponseUpdateInput & { responseId: string };
|
||||
|
||||
@@ -14,7 +14,9 @@ export class ResponseAPI {
|
||||
this.environmentId = environmentId;
|
||||
}
|
||||
|
||||
async create(responseInput: TResponseInput): Promise<Result<TResponse, NetworkError | Error>> {
|
||||
async create(
|
||||
responseInput: Omit<TResponseInput, "environmentId">
|
||||
): Promise<Result<TResponse, NetworkError | Error>> {
|
||||
return makeRequest(this.apiHost, `/api/v1/client/${this.environmentId}/responses`, "POST", responseInput);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "@formbricks/js",
|
||||
"license": "MIT",
|
||||
"version": "1.2.1",
|
||||
"version": "1.2.2",
|
||||
"description": "Formbricks-js allows you to connect your app to Formbricks, display surveys and trigger events.",
|
||||
"keywords": [
|
||||
"Formbricks",
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { TJsActionInput, TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TJsActionInput } from "@formbricks/types/js";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Config } from "./config";
|
||||
import { NetworkError, Result, err, okVoid } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
@@ -58,10 +59,10 @@ export const trackAction = async (
|
||||
return okVoid();
|
||||
};
|
||||
|
||||
export const triggerSurvey = (actionName: string, activeSurveys: TSurveyWithTriggers[]): void => {
|
||||
export const triggerSurvey = (actionName: string, activeSurveys: TSurvey[]): void => {
|
||||
for (const survey of activeSurveys) {
|
||||
for (const trigger of survey.triggers) {
|
||||
if (typeof trigger === "string" ? trigger === actionName : trigger.name === actionName) {
|
||||
if (trigger === actionName) {
|
||||
logger.debug(`Formbricks: survey ${survey.id} triggered by action "${actionName}"`);
|
||||
renderWidget(survey);
|
||||
return;
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TJsPeopleAttributeInput, TJsState } from "@formbricks/types/js";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { TJsState } from "@formbricks/types/js";
|
||||
import { TPerson, TPersonUpdateInput } from "@formbricks/types/people";
|
||||
import { Config } from "./config";
|
||||
import {
|
||||
AttributeAlreadyExistsError,
|
||||
@@ -12,6 +12,7 @@ import {
|
||||
} from "./errors";
|
||||
import { deinitalize, initialize } from "./initialize";
|
||||
import { Logger } from "./logger";
|
||||
import { sync } from "./sync";
|
||||
|
||||
const config = Config.getInstance();
|
||||
const logger = Logger.getInstance();
|
||||
@@ -27,15 +28,16 @@ export const updatePersonAttribute = async (
|
||||
});
|
||||
}
|
||||
|
||||
const input: TJsPeopleAttributeInput = {
|
||||
key,
|
||||
value,
|
||||
const input: TPersonUpdateInput = {
|
||||
attributes: {
|
||||
[key]: value,
|
||||
},
|
||||
};
|
||||
|
||||
const res = await fetch(
|
||||
`${config.get().apiHost}/api/v1/client/${config.get().environmentId}/people/${
|
||||
config.get().state.person?.id
|
||||
}/set-attribute`,
|
||||
config.get().state.person?.userId
|
||||
}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
@@ -57,6 +59,14 @@ export const updatePersonAttribute = async (
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug("Attribute updated. Syncing...");
|
||||
|
||||
await sync({
|
||||
environmentId: config.get().environmentId,
|
||||
apiHost: config.get().apiHost,
|
||||
userId: config.get().state.person?.userId,
|
||||
});
|
||||
|
||||
return ok(resJson.data as TJsState);
|
||||
};
|
||||
|
||||
@@ -95,14 +105,6 @@ export const setPersonAttribute = async (
|
||||
const result = await updatePersonAttribute(key, value.toString());
|
||||
|
||||
if (result.ok) {
|
||||
const state = result.value;
|
||||
|
||||
config.update({
|
||||
apiHost: config.get().apiHost,
|
||||
environmentId: config.get().environmentId,
|
||||
state,
|
||||
});
|
||||
|
||||
return okVoid();
|
||||
}
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
import { ResponseQueue } from "@formbricks/lib/responseQueue";
|
||||
import SurveyState from "@formbricks/lib/surveyState";
|
||||
import { renderSurveyModal } from "@formbricks/surveys";
|
||||
import { TJSStateDisplay, TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TJSStateDisplay } from "@formbricks/types/js";
|
||||
import { TResponseUpdate } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { Config } from "./config";
|
||||
import { ErrorHandler } from "./errors";
|
||||
import { Logger } from "./logger";
|
||||
import { filterPublicSurveys, sync } from "./sync";
|
||||
import { FormbricksAPI } from "@formbricks/api";
|
||||
|
||||
const containerId = "formbricks-web-container";
|
||||
const config = Config.getInstance();
|
||||
@@ -15,7 +16,7 @@ const logger = Logger.getInstance();
|
||||
const errorHandler = ErrorHandler.getInstance();
|
||||
let surveyRunning = false;
|
||||
|
||||
export const renderWidget = (survey: TSurveyWithTriggers) => {
|
||||
export const renderWidget = (survey: TSurvey) => {
|
||||
if (surveyRunning) {
|
||||
logger.debug("A survey is already running. Skipping.");
|
||||
return;
|
||||
@@ -45,7 +46,7 @@ export const renderWidget = (survey: TSurveyWithTriggers) => {
|
||||
const productOverwrites = survey.productOverwrites ?? {};
|
||||
const brandColor = productOverwrites.brandColor ?? product.brandColor;
|
||||
const highlightBorderColor = productOverwrites.highlightBorderColor ?? product.highlightBorderColor;
|
||||
const clickOutside = productOverwrites.clickOutside ?? product.clickOutsideClose;
|
||||
const clickOutside = productOverwrites.clickOutsideClose ?? product.clickOutsideClose;
|
||||
const darkOverlay = productOverwrites.darkOverlay ?? product.darkOverlay;
|
||||
const placement = productOverwrites.placement ?? product.placement;
|
||||
const isBrandingEnabled = product.inAppSurveyBranding;
|
||||
@@ -71,12 +72,13 @@ export const renderWidget = (survey: TSurveyWithTriggers) => {
|
||||
const existingDisplays = config.get().state.displays;
|
||||
const displays = existingDisplays ? [...existingDisplays, localDisplay] : [localDisplay];
|
||||
const previousConfig = config.get();
|
||||
let state = filterPublicSurveys({
|
||||
...previousConfig.state,
|
||||
displays,
|
||||
});
|
||||
config.update({
|
||||
...previousConfig,
|
||||
state: {
|
||||
...previousConfig.state,
|
||||
displays,
|
||||
},
|
||||
state,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -107,18 +109,19 @@ export const renderWidget = (survey: TSurveyWithTriggers) => {
|
||||
if (!lastDisplay.responded) {
|
||||
lastDisplay.responded = true;
|
||||
const previousConfig = config.get();
|
||||
let state = filterPublicSurveys({
|
||||
...previousConfig.state,
|
||||
displays,
|
||||
});
|
||||
config.update({
|
||||
...previousConfig,
|
||||
state: {
|
||||
...previousConfig.state,
|
||||
displays,
|
||||
},
|
||||
state,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (config.get().state.person && config.get().state.person?.id) {
|
||||
surveyState.updatePersonId(config.get().state.person?.id!);
|
||||
if (config.get().state.person && config.get().state.person?.userId) {
|
||||
surveyState.updateUserId(config.get().state.person?.userId!);
|
||||
}
|
||||
responseQueue.updateSurveyState(surveyState);
|
||||
responseQueue.add({
|
||||
|
||||
@@ -116,19 +116,9 @@ export const mockSetEmailIdResponse = () => {
|
||||
fetchMock.mockResponseOnce(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
surveys: [],
|
||||
session: {
|
||||
id: sessionId,
|
||||
createdAt: "2021-03-09T15:00:00.000Z",
|
||||
updatedAt: "2021-03-09T15:00:00.000Z",
|
||||
expiresAt: expiryTime,
|
||||
},
|
||||
noCodeActionClasses: [],
|
||||
person: {
|
||||
id: initialPersonUid,
|
||||
environmentId,
|
||||
attributes: { userId: initialUserId, email: initialUserEmail },
|
||||
},
|
||||
id: initialPersonUid,
|
||||
environmentId,
|
||||
attributes: { userId: initialUserId, email: initialUserEmail },
|
||||
},
|
||||
})
|
||||
);
|
||||
@@ -138,22 +128,12 @@ export const mockSetCustomAttributeResponse = () => {
|
||||
fetchMock.mockResponseOnce(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
surveys: [],
|
||||
session: {
|
||||
id: sessionId,
|
||||
createdAt: "2021-03-09T15:00:00.000Z",
|
||||
updatedAt: "2021-03-09T15:00:00.000Z",
|
||||
expiresAt: expiryTime,
|
||||
},
|
||||
noCodeActionClasses: [],
|
||||
person: {
|
||||
id: initialPersonUid,
|
||||
environmentId,
|
||||
attributes: {
|
||||
userId: initialUserId,
|
||||
email: initialUserEmail,
|
||||
[customAttributeKey]: customAttributeValue,
|
||||
},
|
||||
id: initialPersonUid,
|
||||
environmentId,
|
||||
attributes: {
|
||||
userId: initialUserId,
|
||||
email: initialUserEmail,
|
||||
[customAttributeKey]: customAttributeValue,
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -164,16 +144,12 @@ export const mockUpdateEmailResponse = () => {
|
||||
fetchMock.mockResponseOnce(
|
||||
JSON.stringify({
|
||||
data: {
|
||||
surveys: [],
|
||||
noCodeActionClasses: [],
|
||||
person: {
|
||||
id: initialPersonUid,
|
||||
environmentId,
|
||||
attributes: {
|
||||
userId: initialUserId,
|
||||
email: updatedUserEmail,
|
||||
[customAttributeKey]: customAttributeValue,
|
||||
},
|
||||
id: initialPersonUid,
|
||||
environmentId,
|
||||
attributes: {
|
||||
userId: initialUserId,
|
||||
email: updatedUserEmail,
|
||||
[customAttributeKey]: customAttributeValue,
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
@@ -61,7 +61,7 @@ test("Formbricks should get the current person with no attributes", () => {
|
||||
expect(Object.keys(currentStatePersonAttributes)).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("Formbricks should set email", async () => {
|
||||
/* test("Formbricks should set email", async () => {
|
||||
mockSetEmailIdResponse();
|
||||
await formbricks.setEmail(initialUserEmail);
|
||||
|
||||
@@ -112,7 +112,7 @@ test("Formbricks should update attribute", async () => {
|
||||
expect(email).toStrictEqual(updatedUserEmail);
|
||||
const customAttribute = currentStatePersonAttributes[customAttributeKey];
|
||||
expect(customAttribute).toStrictEqual(customAttributeValue);
|
||||
});
|
||||
}); */
|
||||
|
||||
test("Formbricks should track event", async () => {
|
||||
mockEventTrackResponse();
|
||||
|
||||
@@ -40,7 +40,6 @@ export const getLatestActionByEnvironmentId = async (environmentId: string): Pro
|
||||
const action: TAction = {
|
||||
id: actionPrisma.id,
|
||||
createdAt: actionPrisma.createdAt,
|
||||
// sessionId: actionPrisma.sessionId,
|
||||
personId: actionPrisma.personId,
|
||||
properties: actionPrisma.properties,
|
||||
actionClass: actionPrisma.actionClass,
|
||||
@@ -71,6 +70,60 @@ export const getLatestActionByEnvironmentId = async (environmentId: string): Pro
|
||||
: action;
|
||||
};
|
||||
|
||||
export const getLatestActionByPersonId = async (personId: string): Promise<TAction | null> => {
|
||||
const action = await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([personId, ZId]);
|
||||
|
||||
try {
|
||||
const actionPrisma = await prisma.action.findFirst({
|
||||
where: {
|
||||
personId,
|
||||
},
|
||||
orderBy: {
|
||||
createdAt: "desc",
|
||||
},
|
||||
include: {
|
||||
actionClass: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!actionPrisma) {
|
||||
return null;
|
||||
}
|
||||
const action: TAction = {
|
||||
id: actionPrisma.id,
|
||||
createdAt: actionPrisma.createdAt,
|
||||
personId: actionPrisma.personId,
|
||||
properties: actionPrisma.properties,
|
||||
actionClass: actionPrisma.actionClass,
|
||||
};
|
||||
return action;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError("Database operation failed");
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getLastestActionByPersonId-${personId}`],
|
||||
{
|
||||
tags: [actionCache.tag.byPersonId(personId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
// since the unstable_cache function does not support deserialization of dates, we need to manually deserialize them
|
||||
// https://github.com/vercel/next.js/issues/51613
|
||||
return action
|
||||
? {
|
||||
...action,
|
||||
createdAt: new Date(action.createdAt),
|
||||
}
|
||||
: action;
|
||||
};
|
||||
|
||||
export const getActionsByPersonId = async (personId: string, page?: number): Promise<TAction[]> => {
|
||||
const actions = await unstable_cache(
|
||||
async () => {
|
||||
|
||||
@@ -6,9 +6,11 @@ import {
|
||||
TDisplay,
|
||||
TDisplayCreateInput,
|
||||
TDisplayLegacyCreateInput,
|
||||
TDisplayLegacyUpdateInput,
|
||||
TDisplayUpdateInput,
|
||||
ZDisplayCreateInput,
|
||||
ZDisplayLegacyCreateInput,
|
||||
ZDisplayLegacyUpdateInput,
|
||||
ZDisplayUpdateInput,
|
||||
} from "@formbricks/types/displays";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
@@ -20,6 +22,7 @@ import { getPersonByUserId } from "../person/service";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { displayCache } from "./cache";
|
||||
import { formatDisplaysDateFields } from "./util";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
|
||||
const selectDisplay = {
|
||||
id: true,
|
||||
@@ -30,11 +33,91 @@ const selectDisplay = {
|
||||
personId: true,
|
||||
};
|
||||
|
||||
export const getDisplay = async (displayId: string): Promise<TDisplay | null> =>
|
||||
await unstable_cache(
|
||||
async () => {
|
||||
validateInputs([displayId, ZId]);
|
||||
|
||||
try {
|
||||
const responsePrisma = await prisma.response.findUnique({
|
||||
where: {
|
||||
id: displayId,
|
||||
},
|
||||
select: selectDisplay,
|
||||
});
|
||||
|
||||
return responsePrisma;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
},
|
||||
[`getDisplay-${displayId}`],
|
||||
{
|
||||
tags: [displayCache.tag.byId(displayId)],
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
export const updateDisplay = async (
|
||||
displayId: string,
|
||||
displayInput: Partial<TDisplayUpdateInput>
|
||||
displayInput: TDisplayUpdateInput
|
||||
): Promise<TDisplay> => {
|
||||
validateInputs([displayInput, ZDisplayUpdateInput.partial()]);
|
||||
|
||||
let person: TPerson | null = null;
|
||||
if (displayInput.userId) {
|
||||
person = await getPersonByUserId(displayInput.userId, displayInput.environmentId);
|
||||
if (!person) {
|
||||
throw new ResourceNotFoundError("Person", displayInput.userId);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data = {
|
||||
...(person?.id && {
|
||||
person: {
|
||||
connect: {
|
||||
id: person.id,
|
||||
},
|
||||
},
|
||||
}),
|
||||
...(displayInput.responseId && {
|
||||
responseId: displayInput.responseId,
|
||||
}),
|
||||
};
|
||||
const display = await prisma.display.update({
|
||||
where: {
|
||||
id: displayId,
|
||||
},
|
||||
data,
|
||||
select: selectDisplay,
|
||||
});
|
||||
|
||||
displayCache.revalidate({
|
||||
id: display.id,
|
||||
surveyId: display.surveyId,
|
||||
});
|
||||
|
||||
return display;
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const updateDisplayLegacy = async (
|
||||
displayId: string,
|
||||
displayInput: TDisplayLegacyUpdateInput
|
||||
): Promise<TDisplay> => {
|
||||
validateInputs([displayInput, ZDisplayLegacyUpdateInput]);
|
||||
try {
|
||||
const data = {
|
||||
...(displayInput.personId && {
|
||||
@@ -152,7 +235,7 @@ export const createDisplayLegacy = async (displayInput: TDisplayLegacyCreateInpu
|
||||
}
|
||||
};
|
||||
|
||||
export const markDisplayResponded = async (displayId: string): Promise<TDisplay> => {
|
||||
export const markDisplayRespondedLegacy = async (displayId: string): Promise<TDisplay> => {
|
||||
validateInputs([displayId, ZId]);
|
||||
|
||||
try {
|
||||
|
||||
@@ -10,6 +10,7 @@ import { unstable_cache } from "next/cache";
|
||||
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { personCache } from "./cache";
|
||||
import { createAttributeClass, getAttributeClassByName } from "../attributeClass/service";
|
||||
|
||||
export const selectPerson = {
|
||||
id: true,
|
||||
@@ -224,20 +225,62 @@ export const updatePerson = async (personId: string, personInput: TPersonUpdateI
|
||||
validateInputs([personId, ZId], [personInput, ZPersonUpdateInput]);
|
||||
|
||||
try {
|
||||
const person = await prisma.person.update({
|
||||
where: {
|
||||
id: personId,
|
||||
},
|
||||
data: personInput,
|
||||
select: selectPerson,
|
||||
const person = await getPerson(personId);
|
||||
if (!person) {
|
||||
throw new Error(`Person ${personId} not found`);
|
||||
}
|
||||
|
||||
// Process each attribute
|
||||
const attributeUpdates = Object.entries(personInput.attributes).map(async ([attributeName, value]) => {
|
||||
let attributeClass = await getAttributeClassByName(person.environmentId, attributeName);
|
||||
|
||||
// Create new attribute class if not found
|
||||
if (attributeClass === null) {
|
||||
attributeClass = await createAttributeClass(person.environmentId, attributeName, "code");
|
||||
}
|
||||
|
||||
// Now perform the upsert for the attribute with the found or created attributeClassId
|
||||
await prisma.attribute.upsert({
|
||||
where: {
|
||||
attributeClassId_personId: {
|
||||
attributeClassId: attributeClass!.id,
|
||||
personId,
|
||||
},
|
||||
},
|
||||
update: {
|
||||
value: value.toString(),
|
||||
},
|
||||
create: {
|
||||
attributeClass: {
|
||||
connect: {
|
||||
id: attributeClass!.id,
|
||||
},
|
||||
},
|
||||
person: {
|
||||
connect: {
|
||||
id: personId,
|
||||
},
|
||||
},
|
||||
value: value.toString(),
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// Execute all attribute updates
|
||||
await Promise.all(attributeUpdates);
|
||||
|
||||
personCache.revalidate({
|
||||
id: personId,
|
||||
environmentId: person.environmentId,
|
||||
});
|
||||
|
||||
return transformPrismaPerson(person);
|
||||
const updatedPerson = await getPerson(personId);
|
||||
|
||||
if (!updatedPerson) {
|
||||
throw new Error(`Person ${personId} not found`);
|
||||
}
|
||||
|
||||
return updatedPerson;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
|
||||
export const getPersonIdentifier = (person: TPerson): string | number | null => {
|
||||
return person?.attributes?.userId || person?.attributes?.email || person?.id || null;
|
||||
return person?.userId || person?.attributes?.userId || person?.attributes?.email || person?.id || null;
|
||||
};
|
||||
|
||||
@@ -8,8 +8,10 @@ import { TPerson } from "@formbricks/types/people";
|
||||
import {
|
||||
TResponse,
|
||||
TResponseInput,
|
||||
TResponseLegacyInput,
|
||||
TResponseUpdateInput,
|
||||
ZResponseInput,
|
||||
ZResponseLegacyInput,
|
||||
ZResponseUpdateInput,
|
||||
} from "@formbricks/types/responses";
|
||||
import { TTag } from "@formbricks/types/tags";
|
||||
@@ -17,7 +19,7 @@ import { Prisma } from "@prisma/client";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { deleteDisplayByResponseId } from "../display/service";
|
||||
import { getPerson, transformPrismaPerson } from "../person/service";
|
||||
import { getPerson, getPersonByUserId, transformPrismaPerson } from "../person/service";
|
||||
import { formatResponseDateFields } from "../response/util";
|
||||
import { responseNoteCache } from "../responseNote/cache";
|
||||
import { getResponseNotes } from "../responseNote/service";
|
||||
@@ -195,6 +197,69 @@ export const createResponse = async (responseInput: TResponseInput): Promise<TRe
|
||||
validateInputs([responseInput, ZResponseInput]);
|
||||
captureTelemetry("response created");
|
||||
|
||||
try {
|
||||
let person: TPerson | null = null;
|
||||
|
||||
if (responseInput.userId) {
|
||||
person = await getPersonByUserId(responseInput.userId, responseInput.environmentId);
|
||||
if (!person) {
|
||||
throw new ResourceNotFoundError("Person", responseInput.userId);
|
||||
}
|
||||
}
|
||||
|
||||
const responsePrisma = await prisma.response.create({
|
||||
data: {
|
||||
survey: {
|
||||
connect: {
|
||||
id: responseInput.surveyId,
|
||||
},
|
||||
},
|
||||
finished: responseInput.finished,
|
||||
data: responseInput.data,
|
||||
...(person?.id && {
|
||||
person: {
|
||||
connect: {
|
||||
id: person.id,
|
||||
},
|
||||
},
|
||||
personAttributes: person?.attributes,
|
||||
}),
|
||||
...(responseInput.meta && ({ meta: responseInput?.meta } as Prisma.JsonObject)),
|
||||
singleUseId: responseInput.singleUseId,
|
||||
},
|
||||
select: responseSelection,
|
||||
});
|
||||
|
||||
const response: TResponse = {
|
||||
...responsePrisma,
|
||||
person: responsePrisma.person ? transformPrismaPerson(responsePrisma.person) : null,
|
||||
tags: responsePrisma.tags.map((tagPrisma: { tag: TTag }) => tagPrisma.tag),
|
||||
};
|
||||
|
||||
responseCache.revalidate({
|
||||
id: response.id,
|
||||
personId: response.person?.id,
|
||||
surveyId: response.surveyId,
|
||||
});
|
||||
|
||||
responseNoteCache.revalidate({
|
||||
responseId: response.id,
|
||||
});
|
||||
|
||||
return response;
|
||||
} catch (error) {
|
||||
if (error instanceof Prisma.PrismaClientKnownRequestError) {
|
||||
throw new DatabaseError(error.message);
|
||||
}
|
||||
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
export const createResponseLegacy = async (responseInput: TResponseLegacyInput): Promise<TResponse> => {
|
||||
validateInputs([responseInput, ZResponseLegacyInput]);
|
||||
captureTelemetry("response created");
|
||||
|
||||
try {
|
||||
let person: TPerson | null = null;
|
||||
|
||||
|
||||
@@ -78,7 +78,7 @@ export class ResponseQueue {
|
||||
const response = await this.api.client.response.create({
|
||||
...responseUpdate,
|
||||
surveyId: this.surveyState.surveyId,
|
||||
personId: this.surveyState.personId || null,
|
||||
userId: this.surveyState.userId || null,
|
||||
singleUseId: this.surveyState.singleUseId || null,
|
||||
});
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import "server-only";
|
||||
|
||||
import { prisma } from "@formbricks/database";
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { ZOptionalNumber } from "@formbricks/types/common";
|
||||
import { ZId } from "@formbricks/types/environment";
|
||||
import { DatabaseError, ResourceNotFoundError } from "@formbricks/types/errors";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { TSurvey, TSurveyAttributeFilter, TSurveyInput, ZSurvey } from "@formbricks/types/surveys";
|
||||
import { TActionClass } from "@formbricks/types/actionClasses";
|
||||
import { Prisma } from "@prisma/client";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import { getActionClasses } from "../actionClass/service";
|
||||
import { getAttributeClasses } from "../attributeClass/service";
|
||||
import { ITEMS_PER_PAGE, SERVICES_REVALIDATION_INTERVAL } from "../constants";
|
||||
import { displayCache } from "../display/cache";
|
||||
import { getDisplaysByPersonId } from "../display/service";
|
||||
import { productCache } from "../product/cache";
|
||||
import { getProductByEnvironmentId } from "../product/service";
|
||||
import { responseCache } from "../response/cache";
|
||||
import { captureTelemetry } from "../telemetry";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { formatSurveyDateFields } from "./util";
|
||||
import { surveyCache } from "./cache";
|
||||
import { displayCache } from "../display/cache";
|
||||
import { productCache } from "../product/cache";
|
||||
import { TPerson } from "@formbricks/types/people";
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { getAttributeClasses } from "../attributeClass/service";
|
||||
import { getProductByEnvironmentId } from "../product/service";
|
||||
import { getDisplaysByPersonId } from "../display/service";
|
||||
import { diffInDays } from "../utils/datetime";
|
||||
import { validateInputs } from "../utils/validate";
|
||||
import { surveyCache } from "./cache";
|
||||
import { formatSurveyDateFields } from "./util";
|
||||
|
||||
export const selectSurvey = {
|
||||
id: true,
|
||||
@@ -606,12 +605,87 @@ export const duplicateSurvey = async (environmentId: string, surveyId: string) =
|
||||
return newSurvey;
|
||||
};
|
||||
|
||||
export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
|
||||
export const getSyncSurveys = (environmentId: string, person: TPerson): Promise<TSurvey[]> =>
|
||||
unstable_cache(
|
||||
async () => {
|
||||
return await getSyncSurveys(environmentId, person);
|
||||
const product = await getProductByEnvironmentId(environmentId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
let surveys = await getSurveys(environmentId);
|
||||
|
||||
// filtered surveys for running and web
|
||||
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
|
||||
|
||||
const displays = await getDisplaysByPersonId(person.id);
|
||||
|
||||
// filter surveys that meet the displayOption criteria
|
||||
surveys = surveys.filter((survey) => {
|
||||
if (survey.displayOption === "respondMultiple") {
|
||||
return true;
|
||||
} else if (survey.displayOption === "displayOnce") {
|
||||
return displays.filter((display) => display.surveyId === survey.id).length === 0;
|
||||
} else if (survey.displayOption === "displayMultiple") {
|
||||
return (
|
||||
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null)
|
||||
.length === 0
|
||||
);
|
||||
} else {
|
||||
throw Error("Invalid displayOption");
|
||||
}
|
||||
});
|
||||
|
||||
const attributeClasses = await getAttributeClasses(environmentId);
|
||||
|
||||
// filter surveys that meet the attributeFilters criteria
|
||||
const potentialSurveysWithAttributes = surveys.filter((survey) => {
|
||||
const attributeFilters = survey.attributeFilters;
|
||||
if (attributeFilters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// check if meets all attribute filters criterias
|
||||
return attributeFilters.every((attributeFilter) => {
|
||||
const attributeClassName = attributeClasses.find(
|
||||
(attributeClass) => attributeClass.id === attributeFilter.attributeClassId
|
||||
)?.name;
|
||||
if (!attributeClassName) {
|
||||
throw Error("Invalid attribute filter class");
|
||||
}
|
||||
const personAttributeValue = person.attributes[attributeClassName];
|
||||
if (attributeFilter.condition === "equals") {
|
||||
return personAttributeValue === attributeFilter.value;
|
||||
} else if (attributeFilter.condition === "notEquals") {
|
||||
return personAttributeValue !== attributeFilter.value;
|
||||
} else {
|
||||
throw Error("Invalid attribute filter condition");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const latestDisplay = displays[0];
|
||||
|
||||
// filter surveys that meet the recontactDays criteria
|
||||
surveys = potentialSurveysWithAttributes.filter((survey) => {
|
||||
if (!latestDisplay) {
|
||||
return true;
|
||||
} else if (survey.recontactDays !== null) {
|
||||
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
|
||||
if (!lastDisplaySurvey) {
|
||||
return true;
|
||||
}
|
||||
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
|
||||
} else if (product.recontactDays !== null) {
|
||||
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return surveys;
|
||||
},
|
||||
[`getSyncSurveysCached-${environmentId}`],
|
||||
[`getSyncSurveys-${environmentId}`],
|
||||
{
|
||||
tags: [
|
||||
displayCache.tag.byPersonId(person.id),
|
||||
@@ -621,86 +695,3 @@ export const getSyncSurveysCached = (environmentId: string, person: TPerson) =>
|
||||
revalidate: SERVICES_REVALIDATION_INTERVAL,
|
||||
}
|
||||
)();
|
||||
|
||||
export const getSyncSurveys = async (
|
||||
environmentId: string,
|
||||
person: TPerson
|
||||
): Promise<TSurveyWithTriggers[]> => {
|
||||
// get recontactDays from product
|
||||
const product = await getProductByEnvironmentId(environmentId);
|
||||
|
||||
if (!product) {
|
||||
throw new Error("Product not found");
|
||||
}
|
||||
|
||||
let surveys = await getSurveys(environmentId);
|
||||
|
||||
// filtered surveys for running and web
|
||||
surveys = surveys.filter((survey) => survey.status === "inProgress" && survey.type === "web");
|
||||
|
||||
const displays = await getDisplaysByPersonId(person.id);
|
||||
|
||||
// filter surveys that meet the displayOption criteria
|
||||
surveys = surveys.filter((survey) => {
|
||||
if (survey.displayOption === "respondMultiple") {
|
||||
return true;
|
||||
} else if (survey.displayOption === "displayOnce") {
|
||||
return displays.filter((display) => display.surveyId === survey.id).length === 0;
|
||||
} else if (survey.displayOption === "displayMultiple") {
|
||||
return (
|
||||
displays.filter((display) => display.surveyId === survey.id && display.responseId !== null).length ===
|
||||
0
|
||||
);
|
||||
} else {
|
||||
throw Error("Invalid displayOption");
|
||||
}
|
||||
});
|
||||
|
||||
const attributeClasses = await getAttributeClasses(environmentId);
|
||||
|
||||
// filter surveys that meet the attributeFilters criteria
|
||||
const potentialSurveysWithAttributes = surveys.filter((survey) => {
|
||||
const attributeFilters = survey.attributeFilters;
|
||||
if (attributeFilters.length === 0) {
|
||||
return true;
|
||||
}
|
||||
// check if meets all attribute filters criterias
|
||||
return attributeFilters.every((attributeFilter) => {
|
||||
const attributeClassName = attributeClasses.find(
|
||||
(attributeClass) => attributeClass.id === attributeFilter.attributeClassId
|
||||
)?.name;
|
||||
if (!attributeClassName) {
|
||||
throw Error("Invalid attribute filter class");
|
||||
}
|
||||
const personAttributeValue = person.attributes[attributeClassName];
|
||||
if (attributeFilter.condition === "equals") {
|
||||
return personAttributeValue === attributeFilter.value;
|
||||
} else if (attributeFilter.condition === "notEquals") {
|
||||
return personAttributeValue !== attributeFilter.value;
|
||||
} else {
|
||||
throw Error("Invalid attribute filter condition");
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
const latestDisplay = displays[0];
|
||||
|
||||
// filter surveys that meet the recontactDays criteria
|
||||
surveys = potentialSurveysWithAttributes.filter((survey) => {
|
||||
if (!latestDisplay) {
|
||||
return true;
|
||||
} else if (survey.recontactDays !== null) {
|
||||
const lastDisplaySurvey = displays.filter((display) => display.surveyId === survey.id)[0];
|
||||
if (!lastDisplaySurvey) {
|
||||
return true;
|
||||
}
|
||||
return diffInDays(new Date(), new Date(lastDisplaySurvey.createdAt)) >= survey.recontactDays;
|
||||
} else if (product.recontactDays !== null) {
|
||||
return diffInDays(new Date(), new Date(latestDisplay.createdAt)) >= product.recontactDays;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
return surveys;
|
||||
};
|
||||
|
||||
@@ -3,7 +3,7 @@ import { TResponseUpdate } from "@formbricks/types/responses";
|
||||
export class SurveyState {
|
||||
responseId: string | null = null;
|
||||
displayId: string | null = null;
|
||||
personId: string | null = null;
|
||||
userId: string | null = null;
|
||||
surveyId: string;
|
||||
responseAcc: TResponseUpdate = { finished: false, data: {} };
|
||||
singleUseId: string | null;
|
||||
@@ -12,10 +12,10 @@ export class SurveyState {
|
||||
surveyId: string,
|
||||
singleUseId?: string | null,
|
||||
responseId?: string | null,
|
||||
personId?: string | null
|
||||
userId?: string | null
|
||||
) {
|
||||
this.surveyId = surveyId;
|
||||
this.personId = personId ?? null;
|
||||
this.userId = userId ?? null;
|
||||
this.singleUseId = singleUseId ?? null;
|
||||
this.responseId = responseId ?? null;
|
||||
}
|
||||
@@ -36,7 +36,7 @@ export class SurveyState {
|
||||
this.surveyId,
|
||||
this.singleUseId ?? undefined,
|
||||
this.responseId ?? undefined,
|
||||
this.personId ?? undefined
|
||||
this.userId ?? undefined
|
||||
);
|
||||
copyInstance.responseId = this.responseId;
|
||||
copyInstance.responseAcc = this.responseAcc;
|
||||
@@ -60,11 +60,11 @@ export class SurveyState {
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the person ID
|
||||
* @param id - The person ID
|
||||
* Update the user ID
|
||||
* @param id - The user ID
|
||||
*/
|
||||
updatePersonId(id: string) {
|
||||
this.personId = id;
|
||||
updateUserId(id: string) {
|
||||
this.userId = id;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"include": ["."],
|
||||
"exclude": ["dist", "build", "node_modules"],
|
||||
"compilerOptions": {
|
||||
"baseUrl": ".",
|
||||
"baseUrl": "packages/lib",
|
||||
"paths": {
|
||||
"@/*": ["../../apps/web/*"],
|
||||
"@prisma/client/*": ["@formbricks/database/client/*"]
|
||||
|
||||
@@ -8,7 +8,7 @@ export const validateInputs = (...pairs: ValidationPair[]): void => {
|
||||
const inputValidation = schema.safeParse(value);
|
||||
|
||||
if (!inputValidation.success) {
|
||||
console.error(`Validation failed for ${schema}: ${inputValidation.error.message}`);
|
||||
console.error(`Validation failed for ${JSON.stringify(schema)}: ${inputValidation.error.message}`);
|
||||
throw new ValidationError("Validation failed");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { useEffect, useState } from "preact/hooks";
|
||||
import Progress from "./Progress";
|
||||
import { calculateElementIdx } from "@/lib/utils";
|
||||
|
||||
interface ProgressBarProps {
|
||||
survey: TSurveyWithTriggers;
|
||||
survey: TSurvey;
|
||||
questionId: string;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,7 @@ export default function ProgressBar({ survey, questionId }: ProgressBarProps) {
|
||||
useEffect(() => {
|
||||
// calculate progress
|
||||
setProgress(calculateProgress(questionId, survey, progress));
|
||||
function calculateProgress(questionId: string, survey: TSurveyWithTriggers, progress: number) {
|
||||
function calculateProgress(questionId: string, survey: TSurvey, progress: number) {
|
||||
if (survey.questions.length === 0) return 0;
|
||||
if (questionId === "end") return 1;
|
||||
let currentQustionIdx = survey.questions.findIndex((e) => e.id === questionId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import SubmitButton from "@/components/buttons/SubmitButton";
|
||||
import { calculateElementIdx } from "@/lib/utils";
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import Headline from "./Headline";
|
||||
import HtmlBody from "./HtmlBody";
|
||||
|
||||
@@ -11,7 +11,7 @@ interface WelcomeCardProps {
|
||||
buttonLabel?: string;
|
||||
timeToFinish?: boolean;
|
||||
onSubmit: (data: { [x: string]: any }) => void;
|
||||
survey: TSurveyWithTriggers;
|
||||
survey: TSurvey;
|
||||
}
|
||||
|
||||
const TimerIcon = () => {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
import { useEffect, useRef, useState } from "preact/hooks";
|
||||
import Progress from "../general/Progress";
|
||||
|
||||
interface AutoCloseProps {
|
||||
survey: TSurveyWithTriggers;
|
||||
survey: TSurvey;
|
||||
onClose: () => void;
|
||||
children: any;
|
||||
}
|
||||
|
||||
@@ -101,7 +101,7 @@ export default function Modal({
|
||||
className={cn(
|
||||
getPlacementStyle(placement),
|
||||
show ? "opacity-100" : "opacity-0",
|
||||
"pointer-events-auto absolute bottom-0 h-fit w-full overflow-hidden rounded-lg bg-white shadow-lg ring-1 ring-black ring-opacity-5 transition-all duration-500 ease-in-out sm:m-4 sm:max-w-sm"
|
||||
"border-border pointer-events-auto absolute bottom-0 h-fit w-full overflow-hidden rounded-lg border bg-white shadow-lg transition-all duration-500 ease-in-out sm:m-4 sm:max-w-sm"
|
||||
)}>
|
||||
{!isCenter && (
|
||||
<div class="absolute right-0 top-0 block pr-[1.4rem] pt-2">
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export const cn = (...classes: string[]) => {
|
||||
return classes.filter(Boolean).join(" ");
|
||||
@@ -48,7 +48,7 @@ export const shuffleQuestions = (array: any[], shuffleOption: string) => {
|
||||
return arrayCopy;
|
||||
};
|
||||
|
||||
export const calculateElementIdx = (survey: TSurveyWithTriggers, currentQustionIdx: number): number => {
|
||||
export const calculateElementIdx = (survey: TSurvey, currentQustionIdx: number): number => {
|
||||
const currentQuestion = survey.questions[currentQustionIdx];
|
||||
const surveyLength = survey.questions.length;
|
||||
const middleIdx = Math.floor(surveyLength / 2);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { TSurveyWithTriggers } from "@formbricks/types/js";
|
||||
import { TResponseData, TResponseUpdate } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys";
|
||||
|
||||
export interface SurveyBaseProps {
|
||||
survey: TSurveyWithTriggers;
|
||||
survey: TSurvey;
|
||||
isBrandingEnabled: boolean;
|
||||
activeQuestionId?: string;
|
||||
onDisplay?: () => void;
|
||||
|
||||
@@ -7,30 +7,30 @@ module.exports = {
|
||||
},
|
||||
content: ["./src/**/*.{tsx,ts,jsx,js}"],
|
||||
theme: {
|
||||
colors: {
|
||||
brand: "var(--fb-brand-color)",
|
||||
"on-brand": "var(--fb-brand-text-color)",
|
||||
border: "var(--fb-border-color)",
|
||||
"border-highlight": "var(--fb-border-color-highlight)",
|
||||
focus: "var(--fb-focus-color)",
|
||||
heading: "var(--fb-heading-color)",
|
||||
subheading: "var(--fb-subheading-color)",
|
||||
"info-text": "var(--fb-info-text-color)",
|
||||
signature: "var(--fb-signature-text-color)",
|
||||
"survey-bg": "var(--fb-survey-background-color)",
|
||||
"accent-bg": "var(--fb-accent-background-color)",
|
||||
"accent-selected-bg": "var(--fb-accent-background-color-selected)",
|
||||
placeholder: "var(--fb-placeholder-color)",
|
||||
shadow: "var(--fb-shadow-color)",
|
||||
"rating-fill": "var(--fb-rating-fill)",
|
||||
"rating-focus": "var(--fb-rating-hover)",
|
||||
"rating-selected": "var(--fb-rating-selected)",
|
||||
"back-button-border": "var(--fb-back-btn-border)",
|
||||
"submit-button-border": "var(--fb-submit-btn-border)",
|
||||
"close-button": "var(--fb-close-btn-color)",
|
||||
"close-button-focus": "var(--fb-close-btn-hover-color)",
|
||||
},
|
||||
extend: {
|
||||
colors: {
|
||||
brand: "var(--fb-brand-color)",
|
||||
"on-brand": "var(--fb-brand-text-color)",
|
||||
border: "var(--fb-border-color)",
|
||||
"border-highlight": "var(--fb-border-color-highlight)",
|
||||
focus: "var(--fb-focus-color)",
|
||||
heading: "var(--fb-heading-color)",
|
||||
subheading: "var(--fb-subheading-color)",
|
||||
"info-text": "var(--fb-info-text-color)",
|
||||
signature: "var(--fb-signature-text-color)",
|
||||
"survey-bg": "var(--fb-survey-background-color)",
|
||||
"accent-bg": "var(--fb-accent-background-color)",
|
||||
"accent-selected-bg": "var(--fb-accent-background-color-selected)",
|
||||
placeholder: "var(--fb-placeholder-color)",
|
||||
shadow: "var(--fb-shadow-color)",
|
||||
"rating-fill": "var(--fb-rating-fill)",
|
||||
"rating-focus": "var(--fb-rating-hover)",
|
||||
"rating-selected": "var(--fb-rating-selected)",
|
||||
"back-button-border": "var(--fb-back-btn-border)",
|
||||
"submit-button-border": "var(--fb-submit-btn-border)",
|
||||
"close-button": "var(--fb-close-btn-color)",
|
||||
"close-button-focus": "var(--fb-close-btn-hover-color)",
|
||||
},
|
||||
zIndex: {
|
||||
999999: "999999",
|
||||
},
|
||||
|
||||
@@ -30,12 +30,20 @@ export const ZDisplayLegacyCreateInput = z.object({
|
||||
export type TDisplayLegacyCreateInput = z.infer<typeof ZDisplayLegacyCreateInput>;
|
||||
|
||||
export const ZDisplayUpdateInput = z.object({
|
||||
personId: z.string().cuid().optional(),
|
||||
environmentId: z.string().cuid(),
|
||||
userId: z.string().cuid().optional(),
|
||||
responseId: z.string().cuid().optional(),
|
||||
});
|
||||
|
||||
export type TDisplayUpdateInput = z.infer<typeof ZDisplayUpdateInput>;
|
||||
|
||||
export const ZDisplayLegacyUpdateInput = z.object({
|
||||
personId: z.string().cuid().optional(),
|
||||
responseId: z.string().cuid().optional(),
|
||||
});
|
||||
|
||||
export type TDisplayLegacyUpdateInput = z.infer<typeof ZDisplayLegacyUpdateInput>;
|
||||
|
||||
export const ZDisplaysWithSurveyName = ZDisplay.extend({
|
||||
surveyName: z.string(),
|
||||
});
|
||||
|
||||
@@ -20,7 +20,7 @@ export type TJSStateDisplay = z.infer<typeof ZJSStateDisplay>;
|
||||
|
||||
export const ZJsState = z.object({
|
||||
person: ZPerson.nullable(),
|
||||
surveys: z.array(ZSurveyWithTriggers),
|
||||
surveys: z.array(ZSurvey),
|
||||
noCodeActionClasses: z.array(ZActionClass),
|
||||
product: ZProduct,
|
||||
displays: z.array(ZJSStateDisplay).optional(),
|
||||
@@ -55,7 +55,7 @@ export type TJsSyncInput = z.infer<typeof ZJsSyncInput>;
|
||||
|
||||
export const ZJsSyncLegacyInput = z.object({
|
||||
environmentId: z.string().cuid(),
|
||||
personId: z.string().cuid().optional(),
|
||||
personId: z.string().cuid().optional().or(z.literal("legacy")),
|
||||
sessionId: z.string().cuid().optional(),
|
||||
jsVersion: z.string().optional(),
|
||||
});
|
||||
|
||||
@@ -12,9 +12,10 @@ export const ZPerson = z.object({
|
||||
environmentId: z.string().cuid2(),
|
||||
});
|
||||
|
||||
export type TPerson = z.infer<typeof ZPerson>;
|
||||
|
||||
export const ZPersonUpdateInput = z.object({
|
||||
attributes: ZPersonAttributes,
|
||||
});
|
||||
|
||||
export type TPersonUpdateInput = z.infer<typeof ZPersonUpdateInput>;
|
||||
export type TPerson = z.infer<typeof ZPerson>;
|
||||
|
||||
@@ -66,8 +66,9 @@ export type TResponseDates = {
|
||||
};
|
||||
|
||||
export const ZResponseInput = z.object({
|
||||
environmentId: z.string().cuid2(),
|
||||
surveyId: z.string().cuid2(),
|
||||
personId: z.string().cuid2().nullable(),
|
||||
userId: z.string().nullish(),
|
||||
singleUseId: z.string().nullable().optional(),
|
||||
finished: z.boolean(),
|
||||
data: ZResponseData,
|
||||
@@ -88,6 +89,12 @@ export const ZResponseInput = z.object({
|
||||
|
||||
export type TResponseInput = z.infer<typeof ZResponseInput>;
|
||||
|
||||
export const ZResponseLegacyInput = ZResponseInput.omit({ userId: true, environmentId: true }).extend({
|
||||
personId: z.string().cuid2().nullable(),
|
||||
});
|
||||
|
||||
export type TResponseLegacyInput = z.infer<typeof ZResponseLegacyInput>;
|
||||
|
||||
export const ZResponseUpdateInput = z.object({
|
||||
finished: z.boolean(),
|
||||
data: ZResponseData,
|
||||
|
||||
@@ -37,7 +37,7 @@ export const ZSurveyProductOverwrites = z.object({
|
||||
brandColor: ZColor.nullish(),
|
||||
highlightBorderColor: ZColor.nullish(),
|
||||
placement: ZPlacement.nullish(),
|
||||
clickOutside: z.boolean().nullish(),
|
||||
clickOutsideClose: z.boolean().nullish(),
|
||||
darkOverlay: z.boolean().nullish(),
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user