Merge branch 'main' into shubham/saturn-integration-v2

This commit is contained in:
Johannes
2024-04-16 09:05:37 +02:00
committed by GitHub
18 changed files with 112 additions and 375 deletions

View File

@@ -147,7 +147,7 @@ These variables can be provided at the runtime i.e. in your docker-compose file.
| S3_ACCESS_KEY | Access key for S3. | optional | (resolved by the AWS SDK) |
| S3_SECRET_KEY | Secret key for S3. | optional | (resolved by the AWS SDK) |
| S3_REGION | Region for S3. | optional | (resolved by the AWS SDK) |
| S3_BUCKET | Bucket name for S3. | optional (required if S3 is enabled) | |
| S3_BUCKET_NAME | Bucket name for S3. | optional (required if S3 is enabled) | |
| S3_ENDPOINT | Endpoint for S3. | optional | (resolved by the AWS SDK) |
| PRIVACY_URL | URL for privacy policy. | optional | |
| TERMS_URL | URL for terms of service. | optional | |

View File

@@ -38,9 +38,7 @@ export default async function AttributesSection({ personId }: { personId: string
<div>
<dt className="text-sm font-medium text-slate-500">User Id</dt>
<dd className="ph-no-capture mt-1 text-sm text-slate-900">
{person.attributes.userId ? (
<span>{person.attributes.userId}</span>
) : person.userId ? (
{person.userId ? (
<span>{person.userId}</span>
) : (
<span className="text-slate-300">Not provided</span>
@@ -53,7 +51,7 @@ export default async function AttributesSection({ personId }: { personId: string
</div>
{Object.entries(person.attributes)
.filter(([key, _]) => key !== "email" && key !== "userId" && key !== "language")
.filter(([key, _]) => key !== "email" && key !== "language")
.map(([key, value]) => (
<div key={key}>
<dt className="text-sm font-medium text-slate-500">{capitalizeFirstLetter(key.toString())}</dt>
@@ -62,10 +60,6 @@ export default async function AttributesSection({ personId }: { personId: string
))}
<hr />
<div>
{/* <dt className="text-sm font-medium text-slate-500">Sessions</dt> */}
{/* <dd className="mt-1 text-sm text-slate-900">{numberOfSessions}</dd> */}
</div>
<div>
<dt className="text-sm font-medium text-slate-500">Responses</dt>
<dd className="mt-1 text-sm text-slate-900">{numberOfResponses}</dd>

View File

@@ -4,7 +4,6 @@ import Link from "next/link";
import { ITEMS_PER_PAGE } from "@formbricks/lib/constants";
import { getEnvironment } from "@formbricks/lib/environment/service";
import { getPeople, getPeopleCount } from "@formbricks/lib/person/service";
import { truncateMiddle } from "@formbricks/lib/strings";
import { TPerson } from "@formbricks/types/people";
import { PersonAvatar } from "@formbricks/ui/Avatars";
import EmptySpaceFiller from "@formbricks/ui/EmptySpaceFiller";
@@ -83,9 +82,7 @@ export default async function PeoplePage({
</div>
</div>
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
<div className="ph-no-capture text-slate-900">
{truncateMiddle(getAttributeValue(person, "userId"), 24) || person.userId}
</div>
<div className="ph-no-capture text-slate-900">{person.userId}</div>
</div>
<div className="col-span-2 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
<div className="ph-no-capture text-slate-900">{getAttributeValue(person, "email")}</div>

View File

@@ -15,7 +15,7 @@ export const CTASummary = ({ questionSummary }: CTASummaryProps) => {
<div className="space-y-5 rounded-b-lg bg-white px-4 pb-6 pt-4 text-sm md:px-6 md:text-base">
<div className="text flex justify-between px-2 pb-2">
<div className="mr-8 flex space-x-1">
<p className="font-semibold text-slate-700">Clickthrough Rate (CTR)</p>
<p className="font-semibold text-slate-700">Click-through rate (CTR)</p>
<div>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(questionSummary.ctr.percentage, 1)}%
@@ -23,7 +23,7 @@ export const CTASummary = ({ questionSummary }: CTASummaryProps) => {
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{questionSummary.ctr.count} {questionSummary.ctr.count === 1 ? "response" : "responses"}
{questionSummary.ctr.count} {questionSummary.ctr.count === 1 ? "click" : "clicks"}
</p>
</div>
<ProgressBar barColor="bg-brand" progress={questionSummary.ctr.percentage / 100} />

View File

@@ -1,8 +1,10 @@
import Link from "next/link";
import { useState } from "react";
import { getPersonIdentifier } from "@formbricks/lib/person/util";
import { TSurveyQuestionSummaryMultipleChoice } from "@formbricks/types/surveys";
import { PersonAvatar } from "@formbricks/ui/Avatars";
import { Button } from "@formbricks/ui/Button";
import { ProgressBar } from "@formbricks/ui/ProgressBar";
import { convertFloatToNDecimal } from "../lib/util";
@@ -19,15 +21,28 @@ export const MultipleChoiceSummary = ({
environmentId,
surveyType,
}: MultipleChoiceSummaryProps) => {
const [visibleOtherResponses, setVisibleOtherResponses] = useState(10);
// sort by count and transform to array
const results = Object.values(questionSummary.choices).sort((a, b) => {
if (a.others) return 1; // Always put a after b if a has 'others'
if (b.others) return -1; // Always put b after a if b has 'others'
// Sort by count
return b.count - a.count;
return b.count - a.count; // Sort by count
});
const handleLoadMore = () => {
const lastChoice = results[results.length - 1];
const hasOthers = lastChoice.others && lastChoice.others.length > 0;
if (!hasOthers) return; // If there are no 'others' to show, don't increase the visible options
// Increase the number of visible responses by 10, not exceeding the total number of responses
setVisibleOtherResponses((prevVisibleOptions) =>
Math.min(prevVisibleOptions + 10, lastChoice.others?.length || 0)
);
};
return (
<div className=" rounded-lg border border-slate-200 bg-slate-50 shadow-sm">
<QuestionSummaryHeader questionSummary={questionSummary} />
@@ -58,6 +73,7 @@ export const MultipleChoiceSummary = ({
</div>
{result.others
.filter((otherValue) => otherValue.value !== "")
.slice(0, visibleOtherResponses)
.map((otherValue, idx) => (
<div key={idx}>
{surveyType === "link" && (
@@ -87,6 +103,13 @@ export const MultipleChoiceSummary = ({
)}
</div>
))}
{visibleOtherResponses < result.others.length && (
<div className="flex justify-center py-4">
<Button onClick={handleLoadMore} variant="secondary" size="sm">
Load more
</Button>
</div>
)}
</div>
)}
</div>

View File

@@ -124,7 +124,7 @@ test.describe("JS Package Test", async () => {
await page.waitForTimeout(1000);
await expect(page.getByRole("button", { name: "Responses50%" })).toBeVisible();
await expect(page.getByText("1 Responses", { exact: true }).first()).toBeVisible();
await expect(page.getByText("Clickthrough Rate (CTR)100%")).toBeVisible();
await expect(page.getByText("Click-through rate (CTR)100%")).toBeVisible();
await expect(page.getByText("Somewhat disappointed100%")).toBeVisible();
await expect(page.getByText("Founder100%")).toBeVisible();
await expect(page.getByText("People who believe that PMF").first()).toBeVisible();

View File

@@ -0,0 +1,69 @@
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
async function main() {
await prisma.$transaction(
async (tx) => {
// get all the persons that have an attribute class with the name "userId"
const personsWithUserIdAttribute = await tx.person.findMany({
where: {
attributes: {
some: {
attributeClass: {
name: "userId",
},
},
},
},
include: {
attributes: {
include: { attributeClass: true },
},
},
});
for (let person of personsWithUserIdAttribute) {
// If the person already has a userId, skip it
if (person.userId) {
continue;
}
const userIdAttributeValue = person.attributes.find((attribute) => {
if (attribute.attributeClass.name === "userId") {
return attribute;
}
});
if (!userIdAttributeValue) {
continue;
}
await tx.person.update({
where: {
id: person.id,
},
data: {
userId: userIdAttributeValue.value,
},
});
}
// Delete all attributeClasses with the name "userId"
await tx.attributeClass.deleteMany({
where: {
name: "userId",
},
});
},
{
timeout: 60000 * 3, // 3 minutes
}
);
}
main()
.catch(async (e) => {
console.error(e);
process.exit(1);
})
.finally(async () => await prisma.$disconnect());

View File

@@ -1,293 +0,0 @@
"use strict";
var __awaiter =
(this && this.__awaiter) ||
function (thisArg, _arguments, P, generator) {
function adopt(value) {
return value instanceof P
? value
: new P(function (resolve) {
resolve(value);
});
}
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) {
try {
step(generator.next(value));
} catch (e) {
reject(e);
}
}
function rejected(value) {
try {
step(generator["throw"](value));
} catch (e) {
reject(e);
}
}
function step(result) {
result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected);
}
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __generator =
(this && this.__generator) ||
function (thisArg, body) {
var _ = {
label: 0,
sent: function () {
if (t[0] & 1) throw t[1];
return t[1];
},
trys: [],
ops: [],
},
f,
y,
t,
g;
return (
(g = { next: verb(0), throw: verb(1), return: verb(2) }),
typeof Symbol === "function" &&
(g[Symbol.iterator] = function () {
return this;
}),
g
);
function verb(n) {
return function (v) {
return step([n, v]);
};
}
function step(op) {
if (f) throw new TypeError("Generator is already executing.");
while ((g && ((g = 0), op[0] && (_ = 0)), _))
try {
if (
((f = 1),
y &&
(t =
op[0] & 2
? y["return"]
: op[0]
? y["throw"] || ((t = y["return"]) && t.call(y), 0)
: y.next) &&
!(t = t.call(y, op[1])).done)
)
return t;
if (((y = 0), t)) op = [op[0] & 2, t.value];
switch (op[0]) {
case 0:
case 1:
t = op;
break;
case 4:
_.label++;
return { value: op[1], done: false };
case 5:
_.label++;
y = op[1];
op = [0];
continue;
case 7:
op = _.ops.pop();
_.trys.pop();
continue;
default:
if (!((t = _.trys), (t = t.length > 0 && t[t.length - 1])) && (op[0] === 6 || op[0] === 2)) {
_ = 0;
continue;
}
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) {
_.label = op[1];
break;
}
if (op[0] === 6 && _.label < t[1]) {
_.label = t[1];
t = op;
break;
}
if (t && _.label < t[2]) {
_.label = t[2];
_.ops.push(op);
break;
}
if (t[2]) _.ops.pop();
_.trys.pop();
continue;
}
op = body.call(thisArg, _);
} catch (e) {
op = [6, e];
y = 0;
} finally {
f = t = 0;
}
if (op[0] & 5) throw op[1];
return { value: op[0] ? op[1] : void 0, done: true };
}
};
Object.defineProperty(exports, "__esModule", { value: true });
var cuid2_1 = require("@paralleldrive/cuid2");
var client_1 = require("@prisma/client");
var prisma = new client_1.PrismaClient();
function main() {
return __awaiter(this, void 0, void 0, function () {
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [
4 /*yield*/,
prisma.$transaction(function (tx) {
return __awaiter(_this, void 0, void 0, function () {
var allSurveysWithAttributeFilters;
var _this = this;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [
4 /*yield*/,
prisma.survey.findMany({
where: {
attributeFilters: {
some: {},
},
},
include: {
attributeFilters: { include: { attributeClass: true } },
},
}),
];
case 1:
allSurveysWithAttributeFilters = _a.sent();
if (
!(allSurveysWithAttributeFilters === null || allSurveysWithAttributeFilters === void 0
? void 0
: allSurveysWithAttributeFilters.length)
) {
// stop the migration if there are no surveys with attribute filters
return [2 /*return*/];
}
allSurveysWithAttributeFilters.forEach(function (survey) {
return __awaiter(_this, void 0, void 0, function () {
var attributeFilters, filters;
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
attributeFilters = survey.attributeFilters;
// if there are no attribute filters, we can skip this survey
if (
!(attributeFilters === null || attributeFilters === void 0
? void 0
: attributeFilters.length)
) {
return [2 /*return*/];
}
filters = attributeFilters.map(function (filter, idx) {
var attributeClass = filter.attributeClass;
var resource;
// if the attribute class is userId, we need to create a user segment with the person filter
if (
attributeClass.name === "userId" &&
attributeClass.type === "automatic"
) {
resource = {
id: (0, cuid2_1.createId)(),
root: {
type: "person",
personIdentifier: "userId",
},
qualifier: {
operator: filter.condition,
},
value: filter.value,
};
} else {
resource = {
id: (0, cuid2_1.createId)(),
root: {
type: "attribute",
attributeClassName: attributeClass.name,
},
qualifier: {
operator: filter.condition,
},
value: filter.value,
};
}
var attributeSegment = {
id: filter.id,
connector: idx === 0 ? null : "and",
resource: resource,
};
return attributeSegment;
});
return [
4 /*yield*/,
tx.segment.create({
data: {
title: "".concat(survey.id),
description: "",
isPrivate: true,
filters: filters,
surveys: {
connect: {
id: survey.id,
},
},
environment: {
connect: {
id: survey.environmentId,
},
},
},
}),
];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
});
// delete all attribute filters
return [4 /*yield*/, tx.surveyAttributeFilter.deleteMany({})];
case 2:
// delete all attribute filters
_a.sent();
return [2 /*return*/];
}
});
});
}),
];
case 1:
_a.sent();
return [2 /*return*/];
}
});
});
}
main()
.catch(function (e) {
return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
console.error(e);
process.exit(1);
return [2 /*return*/];
});
});
})
.finally(function () {
return __awaiter(void 0, void 0, void 0, function () {
return __generator(this, function (_a) {
switch (_a.label) {
case 0:
return [4 /*yield*/, prisma.$disconnect()];
case 1:
return [2 /*return*/, _a.sent()];
}
});
});
});

View File

@@ -23,12 +23,13 @@
"lint": "eslint ./src --fix",
"post-install": "pnpm generate",
"predev": "pnpm generate",
"data-migration:v1.6": "ts-node ./migrations/20240207041922_advanced_targeting/data-migration.ts",
"data-migration:styling": "ts-node ./migrations/20240320090315_add_form_styling/data-migration.ts",
"data-migration:v1.6": "ts-node ./data-migrations/20240207041922_advanced_targeting/data-migration.ts",
"data-migration:styling": "ts-node ./data-migrations/20240320090315_add_form_styling/data-migration.ts",
"data-migration:v1.7": "pnpm data-migration:mls && pnpm data-migration:styling",
"data-migration:mls": "ts-node ./migrations/20240318050527_add_languages_and_survey_languages/data-migration.ts",
"data-migration:mls-fix": "ts-node ./migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts",
"data-migration:mls-range-fix": "ts-node ./migrations/20240318050527_add_languages_and_survey_languages/data-migration-range-fix.ts"
"data-migration:mls": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration.ts",
"data-migration:mls-fix": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-fix.ts",
"data-migration:mls-range-fix": "ts-node ./data-migrations/20240318050527_add_languages_and_survey_languages/data-migration-range-fix.ts",
"data-migration:userId": "ts-node ./data-migrations/20240408123456_userid_migration/data-migration.ts"
},
"dependencies": {
"@prisma/client": "^5.12.1",

View File

@@ -35,7 +35,7 @@ export const triggerSurvey = async (survey: TSurvey, action?: string): Promise<v
if (survey.displayPercentage) {
const shouldDisplaySurvey = shouldDisplayBasedOnPercentage(survey.displayPercentage);
if (!shouldDisplaySurvey) {
logger.debug("Survey display skipped based on displayPercentage.");
logger.debug(`Survey display of "${survey.name}" skipped based on displayPercentage.`);
return; // skip displaying the survey
}
}
@@ -50,7 +50,7 @@ const renderWidget = async (survey: TSurvey, action?: string) => {
setIsSurveyRunning(true);
if (survey.delay) {
logger.debug(`Delaying survey by ${survey.delay} seconds.`);
logger.debug(`Delaying survey "${survey.name}" by ${survey.delay} seconds.`);
}
const product = config.get().state.product;
@@ -63,7 +63,7 @@ const renderWidget = async (survey: TSurvey, action?: string) => {
const displayLanguage = getLanguageCode(survey, attributes);
//if survey is not available in selected language, survey wont be shown
if (!displayLanguage) {
logger.debug("Survey not available in specified language.");
logger.debug(`Survey "${survey.name}" is not available in specified language.`);
setIsSurveyRunning(true);
return;
}

View File

@@ -325,50 +325,7 @@ export const getPersonByUserId = async (environmentId: string, userId: string):
return transformPrismaPerson(personWithUserId);
}
// Check if a person with the userId attribute exists
let personWithUserIdAttribute = await prisma.person.findFirst({
where: {
environmentId,
attributes: {
some: {
attributeClass: {
name: "userId",
},
value: userId,
},
},
},
select: selectPerson,
});
const userIdAttributeClassId = personWithUserIdAttribute?.attributes.find(
(attr) => attr.attributeClass.name === "userId" && attr.value === userId
)?.attributeClass.id;
if (!personWithUserIdAttribute) {
return null;
}
personWithUserIdAttribute = await prisma.person.update({
where: {
id: personWithUserIdAttribute.id,
},
data: {
userId,
attributes: {
deleteMany: { attributeClassId: userIdAttributeClassId },
},
},
select: selectPerson,
});
personCache.revalidate({
id: personWithUserIdAttribute.id,
environmentId,
userId,
});
return transformPrismaPerson(personWithUserIdAttribute);
return null;
},
[`getPersonByUserId-${environmentId}-${userId}`],
{
@@ -380,7 +337,7 @@ export const getPersonByUserId = async (environmentId: string, userId: string):
};
/**
* @deprecated This function is deprecated and only used in legacy endpoints. Use updatePerson instead.
* @deprecated This function is deprecated and only used in legacy endpoints. Use `updatePerson` instead.
*/
export const updatePersonAttribute = async (
personId: string,

View File

@@ -14,17 +14,6 @@ export const truncate = (str: string, length: number) => {
return str;
};
// write a function that takes a string and truncates the middle of it so that the beginning and ending are always visible
export const truncateMiddle = (str: string, length: number) => {
if (!str) return "";
if (str.length > length) {
const start = str.substring(0, length / 2);
const end = str.substring(str.length - length / 2, str.length);
return start + " ... " + end;
}
return str;
};
// write a function that takes a string and removes all characters that could cause issues with the url and truncates it to the specified length
export const sanitizeString = (str: string, delimiter: string = "_", length: number = 255) => {
return str.replace(/[^0-9a-zA-Z\-._]+/g, delimiter).substring(0, length);