mirror of
https://github.com/formbricks/formbricks.git
synced 2026-02-05 10:36:06 -06:00
Compare commits
3 Commits
poc-expand
...
01-19-demo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d082c0146c | ||
|
|
a54356c3b0 | ||
|
|
38ea5ed6ae |
@@ -1,176 +0,0 @@
|
||||
import { z } from "zod";
|
||||
import { TResponseData, TResponseDataValue } from "@formbricks/types/responses";
|
||||
import { TSurveyElement } from "@formbricks/types/surveys/elements";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
import { getElementsFromBlocks } from "@/modules/survey/lib/client-utils";
|
||||
|
||||
// Supported expansion keys
|
||||
export const ZResponseExpand = z.enum(["choiceIds", "questionHeadlines"]);
|
||||
|
||||
export type TResponseExpand = z.infer<typeof ZResponseExpand>;
|
||||
|
||||
// Schema for the expand query parameter (comma-separated list)
|
||||
export const ZExpandParam = z
|
||||
.string()
|
||||
.optional()
|
||||
.transform((val) => {
|
||||
if (!val) return [];
|
||||
return val.split(",").map((s) => s.trim());
|
||||
})
|
||||
.pipe(z.array(ZResponseExpand));
|
||||
|
||||
export type TExpandParam = z.infer<typeof ZExpandParam>;
|
||||
|
||||
// Expanded response data structure for a single answer
|
||||
export type TExpandedValue = {
|
||||
value: TResponseDataValue;
|
||||
choiceIds?: string[];
|
||||
};
|
||||
|
||||
// Expanded response data structure
|
||||
export type TExpandedResponseData = {
|
||||
[questionId: string]: TExpandedValue;
|
||||
};
|
||||
|
||||
// Additional expansions that are added as separate fields
|
||||
export type TResponseExpansions = {
|
||||
questionHeadlines?: Record<string, string>;
|
||||
};
|
||||
|
||||
// Choice element types that support choiceIds expansion
|
||||
const CHOICE_ELEMENT_TYPES = ["multipleChoiceMulti", "multipleChoiceSingle", "ranking", "pictureSelection"];
|
||||
|
||||
/**
|
||||
* Check if an element type supports choice ID expansion
|
||||
*/
|
||||
export const isChoiceElement = (element: TSurveyElement): boolean => {
|
||||
return CHOICE_ELEMENT_TYPES.includes(element.type);
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check if element has choices property
|
||||
*/
|
||||
const hasChoices = (
|
||||
element: TSurveyElement
|
||||
): element is TSurveyElement & { choices: Array<{ id: string; label: Record<string, string> }> } => {
|
||||
return "choices" in element && Array.isArray(element.choices);
|
||||
};
|
||||
|
||||
/**
|
||||
* Type guard to check if element has headline property
|
||||
*/
|
||||
const hasHeadline = (
|
||||
element: TSurveyElement
|
||||
): element is TSurveyElement & { headline: Record<string, string> } => {
|
||||
return "headline" in element && typeof element.headline === "object";
|
||||
};
|
||||
|
||||
/**
|
||||
* Extracts choice IDs from a response value for choice-based questions
|
||||
* @param responseValue - The response value (string for single choice, array for multi choice)
|
||||
* @param element - The survey element containing choices
|
||||
* @param language - The language to match against (defaults to "default")
|
||||
* @returns Array of choice IDs
|
||||
*/
|
||||
export const extractChoiceIdsFromResponse = (
|
||||
responseValue: TResponseDataValue,
|
||||
element: TSurveyElement,
|
||||
language: string = "default"
|
||||
): string[] => {
|
||||
if (!isChoiceElement(element) || !responseValue) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// Picture selection already stores IDs directly
|
||||
if (element.type === "pictureSelection") {
|
||||
if (Array.isArray(responseValue)) {
|
||||
return responseValue.filter((id): id is string => typeof id === "string");
|
||||
}
|
||||
return typeof responseValue === "string" ? [responseValue] : [];
|
||||
}
|
||||
|
||||
// For other choice types, we need to map labels to IDs
|
||||
if (!hasChoices(element)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const findChoiceByLabel = (label: string): string => {
|
||||
const choice = element.choices.find((c) => {
|
||||
// Try exact language match first
|
||||
if (c.label[language] === label) {
|
||||
return true;
|
||||
}
|
||||
// Fall back to checking all language values
|
||||
return Object.values(c.label).includes(label);
|
||||
});
|
||||
return choice?.id ?? "other";
|
||||
};
|
||||
|
||||
if (Array.isArray(responseValue)) {
|
||||
return responseValue.filter((v): v is string => typeof v === "string" && v !== "").map(findChoiceByLabel);
|
||||
}
|
||||
|
||||
if (typeof responseValue === "string") {
|
||||
return [findChoiceByLabel(responseValue)];
|
||||
}
|
||||
|
||||
return [];
|
||||
};
|
||||
|
||||
/**
|
||||
* Expand response data with choice IDs
|
||||
* @param data - The response data object
|
||||
* @param survey - The survey definition
|
||||
* @param language - The language code for label matching
|
||||
* @returns Expanded response data with choice IDs
|
||||
*/
|
||||
export const expandWithChoiceIds = (
|
||||
data: TResponseData,
|
||||
survey: TSurvey,
|
||||
language: string = "default"
|
||||
): TExpandedResponseData => {
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const expandedData: TExpandedResponseData = {};
|
||||
|
||||
for (const [questionId, value] of Object.entries(data)) {
|
||||
const element = elements.find((e) => e.id === questionId);
|
||||
|
||||
if (element && isChoiceElement(element)) {
|
||||
const choiceIds = extractChoiceIdsFromResponse(value, element, language);
|
||||
expandedData[questionId] = {
|
||||
value,
|
||||
...(choiceIds.length > 0 && { choiceIds }),
|
||||
};
|
||||
} else {
|
||||
expandedData[questionId] = { value };
|
||||
}
|
||||
}
|
||||
|
||||
return expandedData;
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate question headlines map
|
||||
* @param data - The response data object
|
||||
* @param survey - The survey definition
|
||||
* @param language - The language code for localization
|
||||
* @returns Record mapping question IDs to their headlines
|
||||
*/
|
||||
export const getQuestionHeadlines = (
|
||||
data: TResponseData,
|
||||
survey: TSurvey,
|
||||
language: string = "default"
|
||||
): Record<string, string> => {
|
||||
const elements = getElementsFromBlocks(survey.blocks);
|
||||
const headlines: Record<string, string> = {};
|
||||
|
||||
for (const questionId of Object.keys(data)) {
|
||||
const element = elements.find((e) => e.id === questionId);
|
||||
if (element && hasHeadline(element)) {
|
||||
headlines[questionId] = getLocalizedValue(element.headline, language);
|
||||
}
|
||||
}
|
||||
|
||||
return headlines;
|
||||
};
|
||||
@@ -11,8 +11,7 @@ import { makePartialSchema, responseWithMetaSchema } from "@/modules/api/v2/type
|
||||
export const getResponsesEndpoint: ZodOpenApiOperationObject = {
|
||||
operationId: "getResponses",
|
||||
summary: "Get responses",
|
||||
description:
|
||||
"Gets responses from the database. Use the `expand` parameter to enrich response data with additional information like choice IDs (for language-agnostic processing) or question headlines.",
|
||||
description: "Gets responses from the database.",
|
||||
requestParams: {
|
||||
query: ZGetResponsesFilter.sourceType(),
|
||||
},
|
||||
|
||||
@@ -93,5 +93,4 @@ export const responseFilter: TGetResponsesFilter = {
|
||||
skip: 0,
|
||||
sortBy: "createdAt",
|
||||
order: "asc",
|
||||
expand: [],
|
||||
};
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { Response } from "@prisma/client";
|
||||
import { TResponseData } from "@formbricks/types/responses";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import {
|
||||
TExpandParam,
|
||||
TExpandedResponseData,
|
||||
TResponseExpansions,
|
||||
expandWithChoiceIds,
|
||||
getQuestionHeadlines,
|
||||
} from "./expand";
|
||||
|
||||
export type TTransformedResponse = Omit<Response, "data"> & {
|
||||
data: TResponseData | TExpandedResponseData;
|
||||
expansions?: TResponseExpansions;
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform a response based on requested expansions
|
||||
* @param response - The raw response from the database
|
||||
* @param survey - The survey definition
|
||||
* @param expand - Array of expansion keys to apply
|
||||
* @returns Transformed response with requested expansions
|
||||
*/
|
||||
export const transformResponse = (
|
||||
response: Response,
|
||||
survey: TSurvey,
|
||||
expand: TExpandParam
|
||||
): TTransformedResponse => {
|
||||
const language = response.language ?? "default";
|
||||
const data = response.data as TResponseData;
|
||||
|
||||
let transformedData: TResponseData | TExpandedResponseData = data;
|
||||
const expansions: TResponseExpansions = {};
|
||||
|
||||
// Apply choiceIds expansion
|
||||
if (expand.includes("choiceIds")) {
|
||||
transformedData = expandWithChoiceIds(data, survey, language);
|
||||
}
|
||||
|
||||
// Apply questionHeadlines expansion
|
||||
if (expand.includes("questionHeadlines")) {
|
||||
expansions.questionHeadlines = getQuestionHeadlines(data, survey, language);
|
||||
}
|
||||
|
||||
return {
|
||||
...response,
|
||||
data: transformedData,
|
||||
...(Object.keys(expansions).length > 0 && { expansions }),
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
* Transform multiple responses with caching of survey lookups
|
||||
* @param responses - Array of raw responses from the database
|
||||
* @param expand - Array of expansion keys to apply
|
||||
* @param getSurvey - Function to fetch survey by ID
|
||||
* @returns Array of transformed responses
|
||||
*/
|
||||
export const transformResponses = async (
|
||||
responses: Response[],
|
||||
expand: TExpandParam,
|
||||
getSurvey: (surveyId: string) => Promise<TSurvey | null>
|
||||
): Promise<TTransformedResponse[]> => {
|
||||
if (expand.length === 0) {
|
||||
// No expansion requested, return as-is
|
||||
return responses as TTransformedResponse[];
|
||||
}
|
||||
|
||||
// Cache surveys to avoid duplicate lookups
|
||||
const surveyCache = new Map<string, TSurvey | null>();
|
||||
|
||||
const transformed = await Promise.all(
|
||||
responses.map(async (response) => {
|
||||
let survey = surveyCache.get(response.surveyId);
|
||||
|
||||
if (survey === undefined) {
|
||||
survey = await getSurvey(response.surveyId);
|
||||
surveyCache.set(response.surveyId, survey);
|
||||
}
|
||||
|
||||
if (!survey) {
|
||||
// Survey not found, return response unchanged
|
||||
return response as TTransformedResponse;
|
||||
}
|
||||
|
||||
return transformResponse(response, survey, expand);
|
||||
})
|
||||
);
|
||||
|
||||
return transformed;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Response } from "@prisma/client";
|
||||
import { NextRequest } from "next/server";
|
||||
import { sendToPipeline } from "@/app/lib/pipelines";
|
||||
import { getSurvey } from "@/lib/survey/service";
|
||||
import { authenticatedApiClient } from "@/modules/api/v2/auth/authenticated-api-client";
|
||||
import { validateOtherOptionLengthForMultipleChoice } from "@/modules/api/v2/lib/element";
|
||||
import { responses } from "@/modules/api/v2/lib/response";
|
||||
@@ -13,7 +13,6 @@ import { ApiErrorResponseV2 } from "@/modules/api/v2/types/api-error";
|
||||
import { hasPermission } from "@/modules/organization/settings/api-keys/lib/utils";
|
||||
import { validateFileUploads } from "@/modules/storage/utils";
|
||||
import { createResponseWithQuotaEvaluation, getResponses } from "./lib/response";
|
||||
import { transformResponses } from "./lib/transform";
|
||||
|
||||
export const GET = async (request: NextRequest) =>
|
||||
authenticatedApiClient({
|
||||
@@ -35,17 +34,16 @@ export const GET = async (request: NextRequest) =>
|
||||
(permission) => permission.environmentId
|
||||
);
|
||||
|
||||
const environmentResponses: Response[] = [];
|
||||
const res = await getResponses(environmentIds, query);
|
||||
|
||||
if (!res.ok) {
|
||||
return handleApiError(request, res.error);
|
||||
}
|
||||
|
||||
// Transform responses if expansion is requested
|
||||
const expand = query.expand ?? [];
|
||||
const transformedResponses = await transformResponses(res.data.data, expand, getSurvey);
|
||||
environmentResponses.push(...res.data.data);
|
||||
|
||||
return responses.successResponse({ data: transformedResponses, meta: res.data.meta });
|
||||
return responses.successResponse({ data: environmentResponses });
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { z } from "zod";
|
||||
import { ZResponse } from "@formbricks/database/zod/responses";
|
||||
import { ZExpandParam } from "@/modules/api/v2/management/responses/lib/expand";
|
||||
import { ZGetFilter } from "@/modules/api/v2/types/api-filter";
|
||||
|
||||
export const ZGetResponsesFilter = ZGetFilter.extend({
|
||||
surveyId: z.string().cuid2().optional(),
|
||||
contactId: z.string().optional(),
|
||||
expand: ZExpandParam,
|
||||
}).refine(
|
||||
(data) => {
|
||||
if (data.startDate && data.endDate && data.startDate > data.endDate) {
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "map-pin"
|
||||
src="https://app.formbricks.com/s/m8w91e8wi52pdao8un1f4twu"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "check"
|
||||
src="https://app.formbricks.com/s/orxp15pca6x2nfr3v8pttpwm"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "address-book"
|
||||
src="https://app.formbricks.com/s/z2zjoonfeythx5n6z5qijbsg"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "calendar"
|
||||
src="https://app.formbricks.com/s/rk844spc8ffls25vzkxzzhse"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -15,7 +15,7 @@ icon: "upload"
|
||||
src="https://app.formbricks.com/s/oo4e6vva48w0trn01ht8krwo"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -12,7 +12,7 @@ Free text questions allow respondents to enter a custom answer. Displays a title
|
||||
src="https://app.formbricks.com/s/cm2b2eftv000012b0l3htbu0a"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -11,7 +11,7 @@ The values range from 0 to a user-defined maximum (e.g., 0 to X). The selection
|
||||
src="https://app.formbricks.com/s/obqeey0574jig4lo2gqyv51e"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -10,7 +10,7 @@ icon: "presentation-screen"
|
||||
src="https://app.formbricks.com/s/vqmpasmnt5qcpsa4enheips0"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "ranking-star"
|
||||
src="https://app.formbricks.com/s/z6s84x9wbyk0yqqtfaz238px"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -11,7 +11,7 @@ Rating questions allow respondents to rate questions on a scale. Displays a titl
|
||||
src="https://app.formbricks.com/s/cx7u4n6hwvc3nztuk4vdezl9"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
@@ -38,8 +38,35 @@ Select the icon to be used for the rating scale. The options include: stars, num
|
||||
|
||||
### Range
|
||||
|
||||
Select the range of the rating scale. the options include: 3, 4, 5, 7 or 10. The default is 5.
|
||||
Select the range of the rating scale. the options include: 3, 4, 5, 6, 7 or 10. The default is 5.
|
||||
|
||||
### Labels
|
||||
|
||||
Add labels for the lower and upper bounds of the rating scale. The default is "Not good" and "Very good".
|
||||
|
||||
## CSAT Summary
|
||||
|
||||
After collecting responses, rating questions display a CSAT (Customer Satisfaction) score with a visual traffic light indicator to help you quickly assess satisfaction levels:
|
||||
|
||||
- 🟢 **Green** (> 80%): High satisfaction - your users are very satisfied
|
||||
- 🟠 **Orange** (55-80%): Moderate satisfaction - there's room for improvement
|
||||
- 🔴 **Red** (< 55%): Low satisfaction - immediate attention needed
|
||||
|
||||
<Note>The traffic light indicator appears automatically in the survey summary view, giving you instant feedback on user satisfaction without needing to dig into the data.</Note>
|
||||
|
||||
### How CSAT is Calculated
|
||||
|
||||
The CSAT percentage represents the proportion of respondents who gave a "satisfied" rating. What counts as "satisfied" depends on your selected range:
|
||||
|
||||
| Range | Satisfied Ratings | Examples |
|
||||
|-------|------------------|----------|
|
||||
| 3 | Highest rating only | ⭐⭐⭐ |
|
||||
| 4 | Top 2 ratings | ⭐⭐⭐ or ⭐⭐⭐⭐ |
|
||||
| 5 | Top 2 ratings | ⭐⭐⭐⭐ or ⭐⭐⭐⭐⭐ |
|
||||
| 6 | Top 2 ratings | 5 or 6 |
|
||||
| 7 | Top 2 ratings | 6 or 7 |
|
||||
| 10 | Top 3 ratings | 8, 9, or 10 |
|
||||
|
||||
<Note>
|
||||
**Pro Tip:** For most use cases, a 5-point scale with star or smiley icons provides the best balance between granularity and user experience. Users find it easy to understand and quick to complete.
|
||||
</Note>
|
||||
|
||||
@@ -9,7 +9,7 @@ icon: "calendar-check"
|
||||
src="https://app.formbricks.com/s/hx08x27c2aghywh57rroe6fi"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -12,7 +12,7 @@ Multi select questions allow respondents to select several answers from a list.
|
||||
src="https://app.formbricks.com/s/jhyo6lwzf6eh3fyplhlp7h5f"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -17,7 +17,7 @@ Picture selection questions allow respondents to select one or more images from
|
||||
src="https://app.formbricks.com/s/xtgmwxlk7jxxr4oi6ym7odki"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -12,7 +12,7 @@ Single select questions allow respondents to select one answer from a list. Disp
|
||||
src="https://app.formbricks.com/s/wybd3v3cxpdfve4472fu3lhi"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -11,7 +11,7 @@ It consists of a title (can be Question or Short Note) and a description, which
|
||||
src="https://app.formbricks.com/s/k3p7r7riyy504u4zziqat8zj"
|
||||
style={{
|
||||
position: "relative",
|
||||
height: "90vh",
|
||||
height: "600px",
|
||||
maxHeight: "100vh",
|
||||
width: "100%",
|
||||
border: 0,
|
||||
|
||||
@@ -14,7 +14,7 @@ Recontact options are the last layer of the logic that determines if a survey is
|
||||
|
||||
3. **Recontact Options:** Should the survey be shown (again) to this user? That's dependent on:
|
||||
|
||||
- Did the user see any survey recently (meaning, has Global Waiting Time passed)?
|
||||
- Did the user see any survey recently (meaning, has Survey Cooldown passed)?
|
||||
|
||||
- Did the user see this specific survey already?
|
||||
|
||||
@@ -50,13 +50,13 @@ Available Recontact Options include:
|
||||
|
||||

|
||||
|
||||
## Project-wide Global Waiting Time
|
||||
## Project-wide Survey Cooldown
|
||||
|
||||
The Global Waiting Time is a universal blocker to make sure that no user sees too many surveys. This is particularly helpful when several teams of large organisations use Formbricks at the same time.
|
||||
The Survey Cooldown is a universal blocker to make sure that no user sees too many surveys. This is particularly helpful when several teams of large organisations use Formbricks at the same time.
|
||||
|
||||
<Note>The default Global Waiting Time is set to 7 days.</Note>
|
||||
<Note>The default Survey Cooldown is set to 7 days.</Note>
|
||||
|
||||
To adjust the Global Waiting Time:
|
||||
To adjust the Survey Cooldown:
|
||||
|
||||
1. Visit Formbricks Settings
|
||||
|
||||
@@ -68,9 +68,9 @@ To adjust the Global Waiting Time:
|
||||
|
||||

|
||||
|
||||
## Overriding Global Waiting Time for a Specific Survey
|
||||
## Overriding Survey Cooldown for a Specific Survey
|
||||
|
||||
For specific surveys, you may need to override the default waiting time. Below is how you can do that:
|
||||
For specific surveys, you may need to override the default cooldown. Below is how you can do that:
|
||||
|
||||
1. In the Survey Editor, access the Settings Tab.
|
||||
|
||||
@@ -80,11 +80,11 @@ For specific surveys, you may need to override the default waiting time. Below i
|
||||
|
||||
4. Set a custom recontact period:
|
||||
|
||||
- **Always Show Survey**: Displays the survey whenever triggered, ignoring the waiting time.
|
||||
- **Always Show Survey**: Displays the survey whenever triggered, ignoring the cooldown.
|
||||
|
||||
- **Wait `X` days before showing this survey again**: Sets a specific interval before the survey can be shown again.
|
||||
|
||||

|
||||

|
||||
|
||||
---
|
||||
|
||||
|
||||
30
graphite-demo/server.js
Normal file
30
graphite-demo/server.js
Normal file
@@ -0,0 +1,30 @@
|
||||
const express = require('express');
|
||||
const app = express();
|
||||
const port = 3000;
|
||||
|
||||
// Fake data for the activity feed
|
||||
const activityFeed = [
|
||||
{
|
||||
id: 1000,
|
||||
title: 'New Photo Uploaded',
|
||||
body: 'Alice uploaded a new photo to her album.'
|
||||
},
|
||||
{
|
||||
id: 2000,
|
||||
title: 'Comment on Post',
|
||||
body: "Bob commented on Charlie's post."
|
||||
},
|
||||
{
|
||||
id: 13,
|
||||
title: 'Status Update',
|
||||
body: 'Charlie updated their status: "Excited about the new project!"'
|
||||
}
|
||||
];
|
||||
|
||||
app.get('/feed', (req, res) => {
|
||||
res.json(activityFeed);
|
||||
});
|
||||
|
||||
app.listen(port, () => {
|
||||
console.log(`Server running on port ${port}`);
|
||||
});
|
||||
@@ -0,0 +1,32 @@
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Membership_userId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Project_organizationId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Response_surveyId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Segment_environmentId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."SurveyAttributeFilter_surveyId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."SurveyLanguage_languageId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."SurveyQuota_surveyId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."SurveyTrigger_surveyId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."Tag_environmentId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."TagsOnResponses_responseId_idx";
|
||||
|
||||
-- DropIndex
|
||||
DROP INDEX "public"."User_email_idx";
|
||||
@@ -173,7 +173,6 @@ model Response {
|
||||
@@index([createdAt])
|
||||
@@index([surveyId, createdAt]) // to determine monthly response count
|
||||
@@index([contactId, createdAt]) // to determine monthly identified users (persons)
|
||||
@@index([surveyId])
|
||||
}
|
||||
|
||||
/// Represents a label that can be applied to survey responses.
|
||||
@@ -193,7 +192,6 @@ model Tag {
|
||||
environment Environment @relation(fields: [environmentId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([environmentId, name])
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
/// Junction table linking tags to responses.
|
||||
@@ -208,7 +206,6 @@ model TagsOnResponses {
|
||||
tag Tag @relation(fields: [tagId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@id([responseId, tagId])
|
||||
@@index([responseId])
|
||||
}
|
||||
|
||||
enum SurveyStatus {
|
||||
@@ -259,7 +256,6 @@ model SurveyTrigger {
|
||||
actionClassId String
|
||||
|
||||
@@unique([surveyId, actionClassId])
|
||||
@@index([surveyId])
|
||||
}
|
||||
|
||||
enum SurveyAttributeFilterCondition {
|
||||
@@ -297,7 +293,6 @@ model SurveyAttributeFilter {
|
||||
value String
|
||||
|
||||
@@unique([surveyId, attributeKeyId])
|
||||
@@index([surveyId])
|
||||
@@index([attributeKeyId])
|
||||
}
|
||||
|
||||
@@ -432,7 +427,6 @@ model SurveyQuota {
|
||||
countPartialSubmissions Boolean @default(false)
|
||||
|
||||
@@unique([surveyId, name])
|
||||
@@index([surveyId])
|
||||
}
|
||||
|
||||
/// Junction table linking responses to quotas.
|
||||
@@ -635,7 +629,6 @@ model Project {
|
||||
customHeadScripts String? // Custom HTML scripts for link surveys (self-hosted only)
|
||||
|
||||
@@unique([organizationId, name])
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
/// Represents the top-level organizational hierarchy in Formbricks.
|
||||
@@ -690,7 +683,6 @@ model Membership {
|
||||
role OrganizationRole @default(member)
|
||||
|
||||
@@id([userId, organizationId])
|
||||
@@index([userId])
|
||||
@@index([organizationId])
|
||||
}
|
||||
|
||||
@@ -858,8 +850,6 @@ model User {
|
||||
teamUsers TeamUser[]
|
||||
lastLoginAt DateTime?
|
||||
isActive Boolean @default(true)
|
||||
|
||||
@@index([email])
|
||||
}
|
||||
|
||||
/// Defines a segment of contacts based on attributes.
|
||||
@@ -884,7 +874,6 @@ model Segment {
|
||||
surveys Survey[]
|
||||
|
||||
@@unique([environmentId, title])
|
||||
@@index([environmentId])
|
||||
}
|
||||
|
||||
/// Represents a supported language in the system.
|
||||
@@ -924,7 +913,6 @@ model SurveyLanguage {
|
||||
|
||||
@@id([languageId, surveyId])
|
||||
@@index([surveyId])
|
||||
@@index([languageId])
|
||||
}
|
||||
|
||||
/// Represents a team within an organization.
|
||||
|
||||
Reference in New Issue
Block a user