Compare commits

..

1 Commits

Author SHA1 Message Date
Cursor Agent f12dce9470 fix: use appendChild for inline script injection in CustomScriptsInjector
Replace textContent assignment with appendChild(createTextNode()) to ensure
inline scripts execute properly in all browsers, including Snapchat iOS webview.

Fixes FORMBRICKS-QH
2026-03-05 09:09:09 +00:00
179 changed files with 1394 additions and 1354 deletions
+5
View File
@@ -10,6 +10,9 @@
"build-storybook": "storybook build",
"clean": "rimraf .turbo node_modules dist storybook-static"
},
"dependencies": {
"@formbricks/survey-ui": "workspace:*"
},
"devDependencies": {
"@chromatic-com/storybook": "^5.0.1",
"@storybook/addon-a11y": "10.2.14",
@@ -20,8 +23,10 @@
"@tailwindcss/vite": "4.2.1",
"@typescript-eslint/parser": "8.56.1",
"@vitejs/plugin-react": "5.1.4",
"esbuild": "0.27.3",
"eslint-plugin-react-refresh": "0.4.26",
"eslint-plugin-storybook": "10.2.14",
"prop-types": "15.8.1",
"storybook": "10.2.14",
"vite": "7.3.1",
"@storybook/addon-docs": "10.2.14"
-6
View File
@@ -1,6 +0,0 @@
const baseConfig = require("../../.prettierrc.js");
module.exports = {
...baseConfig,
tailwindConfig: "./tailwind.config.js",
};
@@ -69,7 +69,7 @@ export const ConnectWithFormbricks = ({
) : (
<div className="flex animate-pulse flex-col items-center space-y-4">
<span className="relative flex h-10 w-10">
<span className="absolute inline-flex h-full w-full animate-ping-slow rounded-full bg-slate-400 opacity-75"></span>
<span className="animate-ping-slow absolute inline-flex h-full w-full rounded-full bg-slate-400 opacity-75"></span>
<span className="relative inline-flex h-10 w-10 rounded-full bg-slate-500"></span>
</span>
<p className="pt-4 text-sm font-medium text-slate-600">
@@ -46,7 +46,7 @@ const Page = async (props: ConnectPageProps) => {
channel={channel}
/>
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={`/environments/${environment.id}`}>
@@ -49,7 +49,7 @@ const Page = async (props: XMTemplatePageProps) => {
<XMTemplateList project={project} user={user} environmentId={environment.id} />
{projects.length >= 2 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={`/environments/${environment.id}/surveys`}>
@@ -42,7 +42,7 @@ export const LandingSidebar = ({ user, organization }: LandingSidebarProps) => {
return (
<aside
className={cn(
"z-40 flex w-sidebar-collapsed flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100"
"w-sidebar-collapsed z-40 flex flex-col justify-between rounded-r-xl border-r border-slate-200 bg-white pt-3 shadow-md transition-all duration-100"
)}>
<Image src={FBLogo} width={160} height={30} alt={t("environments.formbricks_logo")} />
@@ -50,7 +50,7 @@ const Page = async (props: ChannelPageProps) => {
<OnboardingOptionsContainer options={channelOptions} />
{projects.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>
@@ -47,7 +47,7 @@ const Page = async (props: ModePageProps) => {
<OnboardingOptionsContainer options={channelOptions} />
{projects.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>
@@ -69,7 +69,7 @@ const Page = async (props: ProjectSettingsPageProps) => {
/>
{projects.length >= 1 && (
<Button
className="absolute right-5 top-5 !mt-0 text-slate-500 hover:text-slate-700"
className="absolute top-5 right-5 !mt-0 text-slate-500 hover:text-slate-700"
variant="ghost"
asChild>
<Link href={"/"}>
@@ -188,7 +188,7 @@ export const MainNavigation = ({
size="icon"
onClick={toggleSidebar}
className={cn(
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:outline-none focus:ring-0 focus:ring-transparent"
"rounded-xl bg-slate-50 p-1 text-slate-600 transition-all hover:bg-slate-100 focus:ring-0 focus:ring-transparent focus:outline-none"
)}>
{isCollapsed ? (
<PanelLeftOpenIcon strokeWidth={1.5} />
@@ -53,7 +53,7 @@ export const WidgetStatusIndicator = ({ environment }: WidgetStatusIndicatorProp
<currentStatus.icon />
</div>
<p className="text-md font-bold text-slate-800 md:text-xl">{currentStatus.title}</p>
<p className="w-2/3 text-balance text-sm text-slate-600">{currentStatus.subtitle}</p>
<p className="w-2/3 text-sm text-balance text-slate-600">{currentStatus.subtitle}</p>
{status === "notImplemented" && (
<Button variant="outline" size="sm" className="bg-white" onClick={() => router.refresh()}>
<RotateCcwIcon />
@@ -98,7 +98,7 @@ export const PasswordConfirmationModal = ({
aria-label="password"
aria-required="true"
required
className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
value={field.value}
onChange={(password) => field.onChange(password)}
/>
@@ -4,7 +4,6 @@ import { ResponseFilterProvider } from "@/app/(app)/environments/[environmentId]
import { getResponseCountBySurveyId } from "@/lib/response/service";
import { getSurvey } from "@/lib/survey/service";
import { authOptions } from "@/modules/auth/lib/authOptions";
import { getTranslate } from "@/lingodotdev/server";
type Props = {
params: Promise<{ surveyId: string; environmentId: string }>;
@@ -15,11 +14,10 @@ export const generateMetadata = async (props: Props): Promise<Metadata> => {
const session = await getServerSession(authOptions);
const survey = await getSurvey(params.surveyId);
const responseCount = await getResponseCountBySurveyId(params.surveyId);
const t = await getTranslate();
if (session) {
return {
title: `${t("common.count_responses", { count: responseCount })} | ${t("environments.surveys.summary.survey_results", { surveyName: survey?.name })}`,
title: `${responseCount} Responses | ${survey?.name} Results`,
};
}
return {
@@ -30,7 +30,8 @@ export const CalSummary = ({ elementSummary, survey }: CalSummaryProps) => {
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{t("common.count_responses", { count: elementSummary.booked.count })}
{elementSummary.booked.count}{" "}
{elementSummary.booked.count === 1 ? t("common.response") : t("common.responses")}
</p>
</div>
<ProgressBar barColor="bg-brand-dark" progress={elementSummary.booked.percentage / 100} />
@@ -46,7 +47,8 @@ export const CalSummary = ({ elementSummary, survey }: CalSummaryProps) => {
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{t("common.count_responses", { count: elementSummary.skipped.count })}
{elementSummary.skipped.count}{" "}
{elementSummary.skipped.count === 1 ? t("common.response") : t("common.responses")}
</p>
</div>
<ProgressBar barColor="bg-brand-dark" progress={elementSummary.skipped.percentage / 100} />
@@ -64,7 +64,7 @@ export const ConsentSummary = ({ elementSummary, survey, setFilter }: ConsentSum
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{t("common.count_responses", { count: summaryItem.count })}
{summaryItem.count} {summaryItem.count === 1 ? t("common.response") : t("common.responses")}
</p>
</div>
<div className="group-hover:opacity-80">
@@ -48,7 +48,7 @@ export const ElementSummaryHeader = ({
{showResponses && (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{t("common.count_responses", { count: elementSummary.responseCount })}
{`${elementSummary.responseCount} ${t("common.responses")}`}
</div>
)}
{additionalInfo}
@@ -41,7 +41,8 @@ export const HiddenFieldsSummary = ({ environment, elementSummary, locale }: Hid
</div>
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{t("common.count_responses", { count: elementSummary.responseCount })}
{elementSummary.responseCount}{" "}
{elementSummary.responseCount === 1 ? t("common.response") : t("common.responses")}
</div>
</div>
</div>
@@ -31,7 +31,7 @@ export const MatrixElementSummary = ({ elementSummary, survey, setFilter }: Matr
if (label) {
return label;
} else if (percentage !== undefined && totalResponsesForRow !== undefined) {
return t("common.count_responses", { count: Math.round((percentage / 100) * totalResponsesForRow) });
return `${Math.round((percentage / 100) * totalResponsesForRow)} ${t("common.responses")}`;
}
return "";
};
@@ -77,7 +77,7 @@ export const MatrixElementSummary = ({ elementSummary, survey, setFilter }: Matr
)}>
<button
style={{ backgroundColor: `rgba(0,196,184,${getOpacityLevel(percentage)})` }}
className="m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline hover:outline-brand-dark"
className="hover:outline-brand-dark m-1 flex h-full w-40 cursor-pointer items-center justify-center rounded p-4 text-sm text-slate-950 hover:outline"
onClick={() =>
setFilter(
elementSummary.element.id,
@@ -75,7 +75,7 @@ export const MultipleChoiceSummary = ({
elementSummary.type === "multipleChoiceMulti" ? (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{t("common.count_selections", { count: elementSummary.selectionCount })}
{`${elementSummary.selectionCount} ${t("common.selections")}`}
</div>
) : undefined
}
@@ -110,7 +110,7 @@ export const MultipleChoiceSummary = ({
</div>
<div className="flex w-full space-x-2">
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
{t("common.count_selections", { count: result.count })}
{result.count} {result.count === 1 ? t("common.selection") : t("common.selections")}
</p>
<p className="rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(result.percentage, 2)}%
@@ -123,7 +123,8 @@ export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProp
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{t("common.count_responses", { count: elementSummary[group]?.count })}
{elementSummary[group]?.count}{" "}
{elementSummary[group]?.count === 1 ? t("common.response") : t("common.responses")}
</p>
</div>
<ProgressBar
@@ -157,7 +158,7 @@ export const NPSSummary = ({ elementSummary, survey, setFilter }: NPSSummaryProp
}>
<div className="flex h-32 w-full flex-col items-center justify-end">
<div
className="w-full rounded-t-lg border border-slate-200 bg-brand-dark transition-all group-hover:brightness-110"
className="bg-brand-dark w-full rounded-t-lg border border-slate-200 transition-all group-hover:brightness-110"
style={{
height: `${Math.max(choice.percentage, 2)}%`,
opacity,
@@ -37,7 +37,7 @@ export const PictureChoiceSummary = ({ elementSummary, survey, setFilter }: Pict
elementSummary.element.allowMulti ? (
<div className="flex items-center rounded-lg bg-slate-100 p-2">
<InboxIcon className="mr-2 h-4 w-4" />
{t("common.count_selections", { count: elementSummary.selectionCount })}
{`${elementSummary.selectionCount} ${t("common.selections")}`}
</div>
) : undefined
}
@@ -74,7 +74,7 @@ export const PictureChoiceSummary = ({ elementSummary, survey, setFilter }: Pict
</div>
<div className="flex w-full space-x-2">
<p className="flex w-full pt-1 text-slate-600 sm:items-end sm:justify-end sm:pt-0">
{t("common.count_selections", { count: result.count })}
{result.count} {result.count === 1 ? t("common.selection") : t("common.selections")}
</p>
<p className="self-end rounded-lg bg-slate-100 px-2 text-slate-700">
{convertFloatToNDecimal(result.percentage, 2)}%
@@ -116,7 +116,7 @@ export const RatingSummary = ({ elementSummary, survey, setFilter }: RatingSumma
)
}>
<div
className={`h-full bg-brand-dark ${isFirst ? "rounded-tl-lg" : ""} ${isLast ? "rounded-tr-lg" : ""}`}
className={`bg-brand-dark h-full ${isFirst ? "rounded-tl-lg" : ""} ${isLast ? "rounded-tr-lg" : ""}`}
style={{ opacity }}
/>
</ClickableBarSegment>
@@ -198,7 +198,7 @@ export const RatingSummary = ({ elementSummary, survey, setFilter }: RatingSumma
</div>
</div>
<p className="flex w-32 items-end justify-end text-slate-600">
{t("common.count_responses", { count: result.count })}
{result.count} {result.count === 1 ? t("common.response") : t("common.responses")}
</p>
</div>
<ProgressBar barColor="bg-brand-dark" progress={result.percentage / 100} />
@@ -215,7 +215,8 @@ export const RatingSummary = ({ elementSummary, survey, setFilter }: RatingSumma
<div className="text flex justify-between px-2">
<p className="font-semibold text-slate-700">{t("common.dismissed")}</p>
<p className="flex w-32 items-end justify-end text-slate-600">
{t("common.count_responses", { count: elementSummary.dismissed.count })}
{elementSummary.dismissed.count}{" "}
{elementSummary.dismissed.count === 1 ? t("common.response") : t("common.responses")}
</p>
</div>
</div>
@@ -105,7 +105,7 @@ export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTa
<div className={scriptsMode === "replace" ? "opacity-50" : ""}>
<FormLabel>{t("environments.surveys.share.custom_html.workspace_scripts_label")}</FormLabel>
<div className="mt-2 max-h-32 overflow-auto rounded-md border border-slate-200 bg-slate-50 p-3">
<pre className="whitespace-pre-wrap font-mono text-xs text-slate-600">
<pre className="font-mono text-xs whitespace-pre-wrap text-slate-600">
{projectCustomScripts}
</pre>
</div>
@@ -135,7 +135,7 @@ export const CustomHtmlTab = ({ projectCustomScripts, isReadOnly }: CustomHtmlTa
rows={8}
placeholder={t("environments.surveys.share.custom_html.placeholder")}
className={cn(
"flex w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:border-brand-dark focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50"
"focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-white px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50"
)}
{...field}
disabled={isReadOnly}
@@ -66,7 +66,7 @@ export const SuccessView: React.FC<SuccessViewProps> = ({
className="relative flex flex-col items-center gap-3 rounded-lg border border-slate-100 bg-white p-4 text-center text-sm text-slate-900 hover:border-slate-200 md:p-8">
<UserIcon className="h-8 w-8 stroke-1 text-slate-900" />
{t("environments.surveys.summary.use_personal_links")}
<Badge size="normal" type="success" className="absolute right-3 top-3" text={t("common.new")} />
<Badge size="normal" type="success" className="absolute top-3 right-3" text={t("common.new")} />
</button>
<Link
href={`/environments/${environmentId}/settings/notifications`}
@@ -192,7 +192,7 @@ export const ElementsComboBox = ({ options, selected, onChangeValue }: ElementCo
value={inputValue}
onValueChange={setInputValue}
placeholder={open ? `${t("common.search")}...` : t("common.select_filter")}
className="max-w-full grow border-none p-0 pl-2 text-sm shadow-none outline-none ring-offset-transparent focus:border-none focus:shadow-none focus:outline-none focus:ring-offset-0"
className="max-w-full grow border-none p-0 pl-2 text-sm shadow-none ring-offset-transparent outline-none focus:border-none focus:shadow-none focus:ring-offset-0 focus:outline-none"
/>
)}
<Button
@@ -10,7 +10,7 @@ const Loading = () => {
<div className="mt-6 p-6">
<GoBackButton />
<div className="mb-6 text-right">
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
<Button className="pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none">
{t("environments.integrations.google_sheets.link_new_sheet")}
</Button>
</div>
@@ -51,7 +51,7 @@ const Loading = () => {
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="text-center"></div>
@@ -10,7 +10,7 @@ const Loading = () => {
<div className="mt-6 p-6">
<GoBackButton />
<div className="mb-6 text-right">
<Button className="pointer-events-none animate-pulse cursor-not-allowed select-none bg-slate-200">
<Button className="pointer-events-none animate-pulse cursor-not-allowed bg-slate-200 select-none">
{t("environments.integrations.notion.link_database")}
</Button>
</div>
@@ -48,7 +48,7 @@ const Loading = () => {
<div className="mt-0 h-4 w-24 animate-pulse rounded-full bg-slate-200"></div>
</div>
</div>
<div className="col-span-2 my-auto flex items-center justify-center whitespace-nowrap text-center text-sm text-slate-500">
<div className="col-span-2 my-auto flex items-center justify-center text-center text-sm whitespace-nowrap text-slate-500">
<div className="h-4 w-16 animate-pulse rounded-full bg-slate-200"></div>
</div>
<div className="text-center"></div>
+108 -106
View File
@@ -6,138 +6,140 @@ export const GET = async (req: NextRequest) => {
let brandColor = req.nextUrl.searchParams.get("brandColor");
return new ImageResponse(
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
height: "100%",
alignItems: "center",
backgroundColor: brandColor ? brandColor + "BF" : "#0000BFBF", // /75 opacity is approximately BF in hex
borderRadius: "0.75rem",
}}>
(
<div
style={{
display: "flex",
flexDirection: "column",
width: "80%",
height: "60%",
backgroundColor: "white",
borderRadius: "0.75rem",
marginTop: "3.25rem",
position: "absolute",
left: "3rem",
top: "0.75rem",
opacity: 0.2,
transform: "rotate(356deg)",
}}></div>
<div
style={{
display: "flex",
flexDirection: "column",
width: "84%",
height: "60%",
backgroundColor: "white",
borderRadius: "0.75rem",
marginTop: "3rem",
position: "absolute",
top: "1.25rem",
left: "3.25rem",
borderWidth: "2px",
opacity: 0.6,
transform: "rotate(357deg)",
}}></div>
<div
style={{
display: "flex",
flexDirection: "column",
width: "85%",
height: "67%",
width: "100%",
height: "100%",
alignItems: "center",
backgroundColor: "white",
backgroundColor: brandColor ? brandColor + "BF" : "#0000BFBF", // /75 opacity is approximately BF in hex
borderRadius: "0.75rem",
marginTop: "2rem",
position: "absolute",
top: "2.3rem",
left: "3.5rem",
transform: "rotate(360deg)",
}}>
<div style={{ display: "flex", flexDirection: "column", width: "100%" }}>
<div
style={{
display: "flex",
flexDirection: "column",
width: "100%",
justifyContent: "space-between",
}}>
<div
style={{
display: "flex",
flexDirection: "column",
width: "80%",
height: "60%",
backgroundColor: "white",
borderRadius: "0.75rem",
marginTop: "3.25rem",
position: "absolute",
left: "3rem",
top: "0.75rem",
opacity: 0.2,
transform: "rotate(356deg)",
}}></div>
<div
style={{
display: "flex",
flexDirection: "column",
width: "84%",
height: "60%",
backgroundColor: "white",
borderRadius: "0.75rem",
marginTop: "3rem",
position: "absolute",
top: "1.25rem",
left: "3.25rem",
borderWidth: "2px",
opacity: 0.6,
transform: "rotate(357deg)",
}}></div>
<div
style={{
display: "flex",
flexDirection: "column",
width: "85%",
height: "67%",
alignItems: "center",
backgroundColor: "white",
borderRadius: "0.75rem",
marginTop: "2rem",
position: "absolute",
top: "2.3rem",
left: "3.5rem",
transform: "rotate(360deg)",
}}>
<div style={{ display: "flex", flexDirection: "column", width: "100%" }}>
<div
style={{
display: "flex",
flexDirection: "column",
paddingLeft: "2rem",
paddingRight: "2rem",
width: "100%",
justifyContent: "space-between",
}}>
<h2
<div
style={{
display: "flex",
flexDirection: "column",
fontSize: "2rem",
fontWeight: "700",
letterSpacing: "-0.025em",
color: "#0f172a",
textAlign: "left",
marginTop: "3.75rem",
paddingLeft: "2rem",
paddingRight: "2rem",
}}>
{name}
</h2>
<h2
style={{
display: "flex",
flexDirection: "column",
fontSize: "2rem",
fontWeight: "700",
letterSpacing: "-0.025em",
color: "#0f172a",
textAlign: "left",
marginTop: "3.75rem",
}}>
{name}
</h2>
</div>
</div>
</div>
<div style={{ display: "flex", justifyContent: "flex-end", marginRight: "2.5rem" }}>
<div
style={{
display: "flex",
borderRadius: "1rem",
position: "absolute",
right: "-0.5rem",
marginTop: "0.5rem",
}}>
<div
content=""
style={{
borderRadius: "0.75rem",
border: "1px solid transparent",
backgroundColor: brandColor ?? "#000",
height: "4.5rem",
width: "9.5rem",
opacity: 0.5,
}}></div>
</div>
<div
style={{
display: "flex",
borderRadius: "1rem",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
}}>
<div style={{ display: "flex", justifyContent: "flex-end", marginRight: "2.5rem" }}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "0.75rem",
border: "1px solid transparent",
backgroundColor: brandColor ?? "#000",
fontSize: "1.5rem",
color: "white",
height: "4.5rem",
width: "9.5rem",
borderRadius: "1rem",
position: "absolute",
right: "-0.5rem",
marginTop: "0.5rem",
}}>
Begin!
<div
content=""
style={{
borderRadius: "0.75rem",
border: "1px solid transparent",
backgroundColor: brandColor ?? "#000",
height: "4.5rem",
width: "9.5rem",
opacity: 0.5,
}}></div>
</div>
<div
style={{
display: "flex",
borderRadius: "1rem",
boxShadow: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
}}>
<div
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "0.75rem",
border: "1px solid transparent",
backgroundColor: brandColor ?? "#000",
fontSize: "1.5rem",
color: "white",
height: "4.5rem",
width: "9.5rem",
}}>
Begin!
</div>
</div>
</div>
</div>
</div>
</div>
</div>,
),
{
width: 800,
height: 400,
+45 -30
View File
@@ -131,11 +131,13 @@ describe("withV1ApiWrapper", () => {
});
test("logs and audits on error response with API key authentication", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -183,11 +185,13 @@ describe("withV1ApiWrapper", () => {
});
test("does not log Sentry if not 500", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -229,11 +233,13 @@ describe("withV1ApiWrapper", () => {
});
test("logs and audits on thrown error", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -285,11 +291,13 @@ describe("withV1ApiWrapper", () => {
});
test("does not log on success response but still audits", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -339,11 +347,13 @@ describe("withV1ApiWrapper", () => {
REDIS_URL: "redis://localhost:6379",
}));
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
const { withV1ApiWrapper } = await import("./with-api-logging");
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
@@ -366,8 +376,9 @@ describe("withV1ApiWrapper", () => {
});
test("handles client-side API routes without authentication", async () => {
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { applyIPRateLimit } = await import("@/modules/core/rate-limit/helpers");
@@ -399,8 +410,9 @@ describe("withV1ApiWrapper", () => {
});
test("returns authentication error for non-client routes without auth", async () => {
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
const { authenticateRequest } = await import("@/app/api/v1/auth");
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -423,8 +435,9 @@ describe("withV1ApiWrapper", () => {
test("handles rate limiting errors", async () => {
const { applyRateLimit } = await import("@/modules/core/rate-limit/helpers");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
const { authenticateRequest } = await import("@/app/api/v1/auth");
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
@@ -449,11 +462,13 @@ describe("withV1ApiWrapper", () => {
});
test("skips audit log creation when no action/targetType provided", async () => {
const { queueAuditEvent: mockedQueueAuditEvent } =
(await import("@/modules/ee/audit-logs/lib/handler")) as unknown as { queueAuditEvent: Mock };
const { queueAuditEvent: mockedQueueAuditEvent } = (await import(
"@/modules/ee/audit-logs/lib/handler"
)) as unknown as { queueAuditEvent: Mock };
const { authenticateRequest } = await import("@/app/api/v1/auth");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } =
await import("@/app/middleware/endpoint-validator");
const { isClientSideApiRoute, isManagementApiRoute, isIntegrationRoute } = await import(
"@/app/middleware/endpoint-validator"
);
vi.mocked(authenticateRequest).mockResolvedValue(mockApiAuthentication);
vi.mocked(isClientSideApiRoute).mockReturnValue({ isClientSideApi: false, isRateLimited: true });
@@ -313,7 +313,7 @@ describe("endpoint-validator", () => {
expect(isPublicDomainRoute("/c")).toBe(false);
expect(isPublicDomainRoute("/contact/token")).toBe(false);
});
test("should return true for pretty URL survey routes", () => {
expect(isPublicDomainRoute("/p/pretty123")).toBe(true);
expect(isPublicDomainRoute("/p/pretty-name-with-dashes")).toBe(true);
+1 -3
View File
@@ -150,9 +150,7 @@ checksums:
common/copy_link: 57a37acfe6d7ed71d00fbbc8079fbb35
common/count_attributes: 042fba9baffef5afe2c24f13d4f50697
common/count_contacts: b1c413a4b06961b71b6aeee95d6775d7
common/count_members: 8cabb9805075f20e3977b919b3b2fdc5
common/count_responses: 690118a456c01c5b4d437ae82b50b131
common/count_selections: c0f581d21468af2f46dad171921f71ba
common/create_new_organization: 51dae7b33143686ee218abf5bea764a5
common/create_segment: 9d8291cd4d778b53b73bbc84fd91c181
common/create_survey: 1cfbba08d34876566d84b2960054a987
@@ -1683,6 +1681,7 @@ checksums:
environments/surveys/edit/welcome_message: 986a434e3895c8ee0b267df95cc40051
environments/surveys/edit/without_a_filter_all_of_your_users_can_be_surveyed: 451990569c61f25d01044cc45b1ce122
environments/surveys/edit/you_have_not_created_a_segment_yet: c6658bd1cee9c5c957c675db044708dd
environments/surveys/edit/you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations: 04241177ba989ef4c1d8c01e1a7b8541
environments/surveys/edit/your_description_here_recall_information_with: 60f73a3cc9bdb9afea2166a7db8fd618
environments/surveys/edit/your_question_here_recall_information_with: 6395bd54f5167830c9d662ba403da167
environments/surveys/edit/your_web_app: 07234bed03a33330dc50ae9fcf0174f3
@@ -1928,7 +1927,6 @@ checksums:
environments/surveys/summary/starts: 3153990a4ade414f501a7e63ab771362
environments/surveys/summary/starts_tooltip: 0a7dd01320490dbbea923053fa1ccad6
environments/surveys/summary/survey_reset_successfully: f53db36a28980ef4766215cf13f01e51
environments/surveys/summary/survey_results: b7d86f636beaee2b4d5746bdda058d07
environments/surveys/summary/this_month: 50845a38865204a97773c44dcd2ebb90
environments/surveys/summary/this_quarter: 9c77d94783dff2269c069389122cd7bd
environments/surveys/summary/this_year: 1e69651c2ac722f8ce138f43cf2e02f9
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Kopieren",
"copy_code": "Code kopieren",
"copy_link": "Link kopieren",
"count_attributes": "{count, plural, one {{count} Attribut} other {{count} Attribute}}",
"count_contacts": "{count, plural, one {{count} Kontakt} other {{count} Kontakte}}",
"count_members": "{count, plural, one {{count} Mitglied} other {{count} Mitglieder}}",
"count_responses": "{count, plural, one {{count} Antwort} other {{count} Antworten}}",
"count_selections": "{count, plural, one {{count} Auswahl} other {{count} Auswahlen}}",
"count_attributes": "{value, plural, one {{value} Attribut} other {{value} Attribute}}",
"count_contacts": "{value, plural, one {{value} Kontakt} other {{value} Kontakte}}",
"count_responses": "{value, plural, one {{value} Antwort} other {{value} Antworten}}",
"create_new_organization": "Neue Organisation erstellen",
"create_segment": "Segment erstellen",
"create_survey": "Umfrage erstellen",
@@ -276,6 +274,7 @@
"look_and_feel": "Darstellung",
"manage": "Verwalten",
"marketing": "Marketing",
"member": "Mitglied",
"members": "Mitglieder",
"members_and_teams": "Mitglieder & Teams",
"membership_not_found": "Mitgliedschaft nicht gefunden",
@@ -383,6 +382,8 @@
"select_teams": "Teams auswählen",
"selected": "Ausgewählt",
"selected_questions": "Ausgewählte Fragen",
"selection": "Auswahl",
"selections": "Auswahlen",
"send_test_email": "Test-E-Mail senden",
"session_not_found": "Sitzung nicht gefunden",
"settings": "Einstellungen",
@@ -1755,6 +1756,7 @@
"welcome_message": "Willkommensnachricht",
"without_a_filter_all_of_your_users_can_be_surveyed": "Ohne Filter können alle deine Nutzer befragt werden.",
"you_have_not_created_a_segment_yet": "Du hast noch keinen Segment erstellt.",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Sie müssen zwei oder mehr Sprachen in Ihrem Workspace eingerichtet haben, um mit Übersetzungen zu arbeiten.",
"your_description_here_recall_information_with": "Deine Beschreibung hier. Informationen abrufen mit @",
"your_question_here_recall_information_with": "Deine Frage hier. Informationen abrufen mit @",
"your_web_app": "Deine Web-App",
@@ -2028,7 +2030,6 @@
"starts": "Startet",
"starts_tooltip": "So oft wurde die Umfrage gestartet.",
"survey_reset_successfully": "Umfrage erfolgreich zurückgesetzt! {responseCount} Antworten und {displayCount} Anzeigen wurden gelöscht.",
"survey_results": "{surveyName}-Ergebnisse",
"this_month": "Dieser Monat",
"this_quarter": "Dieses Quartal",
"this_year": "Dieses Jahr",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Copy",
"copy_code": "Copy code",
"copy_link": "Copy Link",
"count_attributes": "{count, plural, one {{count} attribute} other {{count} attributes}}",
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacts}}",
"count_members": "{count, plural, one {{count} member} other {{count} members}}",
"count_responses": "{count, plural, one {{count} response} other {{count} responses}}",
"count_selections": "{count, plural, one {{count} selection} other {{count} selections}}",
"count_attributes": "{value, plural, one {{value} attribute} other {{value} attributes}}",
"count_contacts": "{value, plural, one {{value} contact} other {{value} contacts}}",
"count_responses": "{value, plural, one {{value} response} other {{value} responses}}",
"create_new_organization": "Create new organization",
"create_segment": "Create segment",
"create_survey": "Create survey",
@@ -276,6 +274,7 @@
"look_and_feel": "Look & Feel",
"manage": "Manage",
"marketing": "Marketing",
"member": "Member",
"members": "Members",
"members_and_teams": "Members & Teams",
"membership_not_found": "Membership not found",
@@ -383,6 +382,8 @@
"select_teams": "Select teams",
"selected": "Selected",
"selected_questions": "Selected questions",
"selection": "Selection",
"selections": "Selections",
"send_test_email": "Send test email",
"session_not_found": "Session not found",
"settings": "Settings",
@@ -1755,6 +1756,7 @@
"welcome_message": "Welcome message",
"without_a_filter_all_of_your_users_can_be_surveyed": "Without a filter, all of your users can be surveyed.",
"you_have_not_created_a_segment_yet": "You have not created a segment yet",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "You need to have two or more languages set up in your workspace to work with translations.",
"your_description_here_recall_information_with": "Your description here. Recall information with @",
"your_question_here_recall_information_with": "Your question here. Recall information with @",
"your_web_app": "Your web app",
@@ -2028,7 +2030,6 @@
"starts": "Starts",
"starts_tooltip": "Number of times the survey has been started.",
"survey_reset_successfully": "Survey reset successfully. {responseCount} responses and {displayCount} displays were deleted.",
"survey_results": "{surveyName} Results",
"this_month": "This month",
"this_quarter": "This quarter",
"this_year": "This year",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Copiar",
"copy_code": "Copiar código",
"copy_link": "Copiar enlace",
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
"count_contacts": "{count, plural, one {{count} contacto} other {{count} contactos}}",
"count_members": "{count, plural, one {{count} miembro} other {{count} miembros}}",
"count_responses": "{count, plural, one {{count} respuesta} other {{count} respuestas}}",
"count_selections": "{count, plural, one {{count} selección} other {{count} selecciones}}",
"count_attributes": "{value, plural, one {{value} atributo} other {{value} atributos}}",
"count_contacts": "{value, plural, one {{value} contacto} other {{value} contactos}}",
"count_responses": "{value, plural, one {{value} respuesta} other {{value} respuestas}}",
"create_new_organization": "Crear organización nueva",
"create_segment": "Crear segmento",
"create_survey": "Crear encuesta",
@@ -276,6 +274,7 @@
"look_and_feel": "Apariencia",
"manage": "Gestionar",
"marketing": "Marketing",
"member": "Miembro",
"members": "Miembros",
"members_and_teams": "Miembros y equipos",
"membership_not_found": "Membresía no encontrada",
@@ -383,6 +382,8 @@
"select_teams": "Seleccionar equipos",
"selected": "Seleccionado",
"selected_questions": "Preguntas seleccionadas",
"selection": "Selección",
"selections": "Selecciones",
"send_test_email": "Enviar correo electrónico de prueba",
"session_not_found": "Sesión no encontrada",
"settings": "Ajustes",
@@ -1755,6 +1756,7 @@
"welcome_message": "Mensaje de bienvenida",
"without_a_filter_all_of_your_users_can_be_surveyed": "Sin un filtro, todos tus usuarios pueden ser encuestados.",
"you_have_not_created_a_segment_yet": "Aún no has creado un segmento",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Necesitas tener dos o más idiomas configurados en tu proyecto para trabajar con traducciones.",
"your_description_here_recall_information_with": "Tu descripción aquí. Recupera información con @",
"your_question_here_recall_information_with": "Tu pregunta aquí. Recupera información con @",
"your_web_app": "Tu aplicación web",
@@ -2028,7 +2030,6 @@
"starts": "Inicios",
"starts_tooltip": "Número de veces que se ha iniciado la encuesta.",
"survey_reset_successfully": "¡Encuesta restablecida correctamente! Se eliminaron {responseCount} respuestas y {displayCount} visualizaciones.",
"survey_results": "Resultados de {surveyName}",
"this_month": "Este mes",
"this_quarter": "Este trimestre",
"this_year": "Este año",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Copier",
"copy_code": "Copier le code",
"copy_link": "Copier le lien",
"count_attributes": "{count, plural, one {{count} attribut} other {{count} attributs}}",
"count_contacts": "{count, plural, one {# contact} other {# contacts} }",
"count_members": "{count, plural, one {{count} membre} other {{count} membres}}",
"count_responses": "{count, plural, other {# réponses}}",
"count_selections": "{count, plural, one {{count} sélection} other {{count} sélections}}",
"count_attributes": "{value, plural, one {{value} attribut} other {{value} attributs}}",
"count_contacts": "{value, plural, one {# contact} other {# contacts} }",
"count_responses": "{value, plural, other {# réponses}}",
"create_new_organization": "Créer une nouvelle organisation",
"create_segment": "Créer un segment",
"create_survey": "Créer un sondage",
@@ -276,6 +274,7 @@
"look_and_feel": "Apparence",
"manage": "Gérer",
"marketing": "Marketing",
"member": "Membre",
"members": "Membres",
"members_and_teams": "Membres & Équipes",
"membership_not_found": "Abonnement non trouvé",
@@ -383,6 +382,8 @@
"select_teams": "Sélectionner les équipes",
"selected": "Sélectionné",
"selected_questions": "Questions sélectionnées",
"selection": "Sélection",
"selections": "Sélections",
"send_test_email": "Envoyer un e-mail de test",
"session_not_found": "Session non trouvée",
"settings": "Paramètres",
@@ -1755,6 +1756,7 @@
"welcome_message": "Message de bienvenue",
"without_a_filter_all_of_your_users_can_be_surveyed": "Sans filtre, tous vos utilisateurs peuvent être sondés.",
"you_have_not_created_a_segment_yet": "Tu n'as pas encore créé de segment.",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Vous devez avoir deux langues ou plus configurées dans votre espace de travail pour travailler avec les traductions.",
"your_description_here_recall_information_with": "Votre description ici. Rappelez-vous des informations avec @",
"your_question_here_recall_information_with": "Votre question ici. Rappelez-vous des informations avec @",
"your_web_app": "Votre application web",
@@ -2028,7 +2030,6 @@
"starts": "Commence",
"starts_tooltip": "Nombre de fois que l'enquête a été commencée.",
"survey_reset_successfully": "Réinitialisation du sondage réussie ! {responseCount} réponses et {displayCount} affichages ont été supprimés.",
"survey_results": "Résultats de {surveyName}",
"this_month": "Ce mois-ci",
"this_quarter": "Ce trimestre",
"this_year": "Cette année",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Másolás",
"copy_code": "Kód másolása",
"copy_link": "Hivatkozás másolása",
"count_attributes": "{count, plural, one {{count} attribútum} other {{count} attribútum}}",
"count_contacts": "{count, plural, one {{count} partner} other {{count} partner}}",
"count_members": "{count, plural, one {{count} tag} other {{count} tag}}",
"count_responses": "{count, plural, one {{count} válasz} other {{count} válasz}}",
"count_selections": "{count, plural, one {{count} kijelölés} other {{count} kijelölés}}",
"count_attributes": "{value, plural, one {{value} attribútum} other {{value} attribútum}}",
"count_contacts": "{value, plural, one {{value} partner} other {{value} partner}}",
"count_responses": "{value, plural, one {{value} válasz} other {{value} válasz}}",
"create_new_organization": "Új szervezet létrehozása",
"create_segment": "Szakasz létrehozása",
"create_survey": "Kérdőív létrehozása",
@@ -276,6 +274,7 @@
"look_and_feel": "Megjelenés",
"manage": "Kezelés",
"marketing": "Marketing",
"member": "Tag",
"members": "Tagok",
"members_and_teams": "Tagok és csapatok",
"membership_not_found": "A tagság nem található",
@@ -383,6 +382,8 @@
"select_teams": "Csapatok kiválasztása",
"selected": "Kiválasztva",
"selected_questions": "Kiválasztott kérdések",
"selection": "Kiválasztás",
"selections": "Kiválasztások",
"send_test_email": "Teszt e-mail küldése",
"session_not_found": "A munkamenet nem található",
"settings": "Beállítások",
@@ -1755,6 +1756,7 @@
"welcome_message": "Üdvözlő üzenet",
"without_a_filter_all_of_your_users_can_be_surveyed": "Szűrő nélkül az összes felhasználója megkérdezhető.",
"you_have_not_created_a_segment_yet": "Még nem hozott létre szakaszt",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Be kell állítania kettő vagy több nyelvet a munkaterületen a fordításokkal való munkához.",
"your_description_here_recall_information_with": "Ide jön a leírás. Információk visszahívása a @ karakterrel.",
"your_question_here_recall_information_with": "Ide jön a kérdés. Információk visszahívása a @ karakterrel.",
"your_web_app": "Saját webalkalmazás",
@@ -2028,7 +2030,6 @@
"starts": "Elkezdések",
"starts_tooltip": "A kérdőív elkezdési alkalmainak száma.",
"survey_reset_successfully": "A kérdőív sikeresen visszaállítva. {responseCount} válasz és {displayCount} megjelenítés lett törölve.",
"survey_results": "{surveyName} eredményei",
"this_month": "Ez a hónap",
"this_quarter": "Ez a negyedév",
"this_year": "Ez az év",
+5 -4
View File
@@ -175,11 +175,9 @@
"copy": "コピー",
"copy_code": "コードをコピー",
"copy_link": "リンクをコピー",
"count_attributes": "{count, plural, other {{count}個の属性}}",
"count_attributes": "{value, plural, other {{value}個の属性}}",
"count_contacts": "{count, plural, other {# 件の連絡先}}",
"count_members": "{count, plural, other {{count}人のメンバー}}",
"count_responses": "{count, plural, other {# 件の回答}}",
"count_selections": "{count, plural, other {{count}件選択中}}",
"create_new_organization": "新しい組織を作成",
"create_segment": "セグメントを作成",
"create_survey": "フォームを作成",
@@ -276,6 +274,7 @@
"look_and_feel": "デザイン",
"manage": "管理",
"marketing": "マーケティング",
"member": "メンバー",
"members": "メンバー",
"members_and_teams": "メンバー&チーム",
"membership_not_found": "メンバーシップが見つかりません",
@@ -383,6 +382,8 @@
"select_teams": "チームを選択",
"selected": "選択済み",
"selected_questions": "選択した質問",
"selection": "選択",
"selections": "選択",
"send_test_email": "テストメールを送信",
"session_not_found": "セッションが見つかりません",
"settings": "設定",
@@ -1755,6 +1756,7 @@
"welcome_message": "ウェルカムメッセージ",
"without_a_filter_all_of_your_users_can_be_surveyed": "フィルターがなければ、すべてのユーザーがフォームに回答できます。",
"you_have_not_created_a_segment_yet": "まだセグメントを作成していません",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "翻訳を使用するには、ワークスペースに2つ以上の言語を設定する必要があります。",
"your_description_here_recall_information_with": "ここにあなたの説明。@ で情報を呼び出す",
"your_question_here_recall_information_with": "ここにあなたの質問。@ で情報を呼び出す",
"your_web_app": "あなたのウェブアプリ",
@@ -2028,7 +2030,6 @@
"starts": "開始",
"starts_tooltip": "フォームが開始された回数。",
"survey_reset_successfully": "フォームを正常にリセットしました!{responseCount} 件の回答と {displayCount} 件の表示が削除されました。",
"survey_results": "{surveyName}の結果",
"this_month": "今月",
"this_quarter": "今四半期",
"this_year": "今年",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Kopiëren",
"copy_code": "Kopieer code",
"copy_link": "Kopieer link",
"count_attributes": "{count, plural, one {{count} attribuut} other {{count} attributen}}",
"count_contacts": "{count, plural, one {{count} contact} other {{count} contacten}}",
"count_members": "{count, plural, one {{count} lid} other {{count} leden}}",
"count_responses": "{count, plural, one {{count} reactie} other {{count} reacties}}",
"count_selections": "{count, plural, one {{count} selectie} other {{count} selecties}}",
"count_attributes": "{value, plural, one {{value} attribuut} other {{value} attributen}}",
"count_contacts": "{value, plural, one {{value} contact} other {{value} contacten}}",
"count_responses": "{value, plural, one {{value} reactie} other {{value} reacties}}",
"create_new_organization": "Creëer een nieuwe organisatie",
"create_segment": "Segment maken",
"create_survey": "Enquête maken",
@@ -276,6 +274,7 @@
"look_and_feel": "Kijk & voel",
"manage": "Beheren",
"marketing": "Marketing",
"member": "Lid",
"members": "Leden",
"members_and_teams": "Leden & teams",
"membership_not_found": "Lidmaatschap niet gevonden",
@@ -383,6 +382,8 @@
"select_teams": "Selecteer teams",
"selected": "Gekozen",
"selected_questions": "Geselecteerde vragen",
"selection": "Selectie",
"selections": "Selecties",
"send_test_email": "Test-e-mail verzenden",
"session_not_found": "Sessie niet gevonden",
"settings": "Instellingen",
@@ -1755,6 +1756,7 @@
"welcome_message": "Welkomstbericht",
"without_a_filter_all_of_your_users_can_be_surveyed": "Zonder filter kunnen al uw gebruikers worden bevraagd.",
"you_have_not_created_a_segment_yet": "U heeft nog geen segment aangemaakt",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Je moet twee of meer talen hebben ingesteld in je werkruimte om met vertalingen te kunnen werken.",
"your_description_here_recall_information_with": "Uw beschrijving hier. Roep informatie op met @",
"your_question_here_recall_information_with": "Uw vraag hier. Roep informatie op met @",
"your_web_app": "Uw web-app",
@@ -2028,7 +2030,6 @@
"starts": "Begint",
"starts_tooltip": "Aantal keren dat de enquête is gestart.",
"survey_reset_successfully": "Enquête opnieuw ingesteld! {responseCount} reacties en {displayCount} displays zijn verwijderd.",
"survey_results": "Resultaten van {surveyName}",
"this_month": "Deze maand",
"this_quarter": "Dit kwartaal",
"this_year": "Dit jaar",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Copiar",
"copy_code": "Copiar código",
"copy_link": "Copiar Link",
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
"count_contacts": "{count, plural, one {# contato} other {# contatos} }",
"count_members": "{count, plural, one {{count} membro} other {{count} membros}}",
"count_responses": "{count, plural, other {# respostas}}",
"count_selections": "{count, plural, one {{count} seleção} other {{count} seleções}}",
"count_attributes": "{value, plural, one {{value} atributo} other {{value} atributos}}",
"count_contacts": "{value, plural, one {# contato} other {# contatos} }",
"count_responses": "{value, plural, other {# respostas}}",
"create_new_organization": "Criar nova organização",
"create_segment": "Criar segmento",
"create_survey": "Criar pesquisa",
@@ -276,6 +274,7 @@
"look_and_feel": "Aparência e Experiência",
"manage": "gerenciar",
"marketing": "marketing",
"member": "Membros",
"members": "Membros",
"members_and_teams": "Membros e equipes",
"membership_not_found": "Assinatura não encontrada",
@@ -383,6 +382,8 @@
"select_teams": "Selecionar times",
"selected": "Selecionado",
"selected_questions": "Perguntas selecionadas",
"selection": "seleção",
"selections": "seleções",
"send_test_email": "Enviar e-mail de teste",
"session_not_found": "Sessão não encontrada",
"settings": "Configurações",
@@ -1755,6 +1756,7 @@
"welcome_message": "Mensagem de boas-vindas",
"without_a_filter_all_of_your_users_can_be_surveyed": "Sem um filtro, todos os seus usuários podem ser pesquisados.",
"you_have_not_created_a_segment_yet": "Você ainda não criou um segmento.",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Você precisa ter dois ou mais idiomas configurados em seu espaço de trabalho para trabalhar com traduções.",
"your_description_here_recall_information_with": "Sua descrição aqui. Lembre-se de informações com @",
"your_question_here_recall_information_with": "Sua pergunta aqui. Lembre-se de informações com @",
"your_web_app": "Sua aplicação web",
@@ -2028,7 +2030,6 @@
"starts": "começa",
"starts_tooltip": "Número de vezes que a pesquisa foi iniciada.",
"survey_reset_successfully": "Pesquisa redefinida com sucesso! {responseCount} respostas e {displayCount} exibições foram deletadas.",
"survey_results": "Resultados de {surveyName}",
"this_month": "Este mês",
"this_quarter": "Este trimestre",
"this_year": "Este ano",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Copiar",
"copy_code": "Copiar código",
"copy_link": "Copiar Link",
"count_attributes": "{count, plural, one {{count} atributo} other {{count} atributos}}",
"count_contacts": "{count, plural, one {# contacto} other {# contactos} }",
"count_members": "{count, plural, one {{count} membro} other {{count} membros}}",
"count_responses": "{count, plural, other {# respostas}}",
"count_selections": "{count, plural, one {{count} seleção} other {{count} seleções}}",
"count_attributes": "{value, plural, one {{value} atributo} other {{value} atributos}}",
"count_contacts": "{value, plural, one {# contacto} other {# contactos} }",
"count_responses": "{value, plural, other {# respostas}}",
"create_new_organization": "Criar nova organização",
"create_segment": "Criar segmento",
"create_survey": "Criar inquérito",
@@ -276,6 +274,7 @@
"look_and_feel": "Aparência e Sensação",
"manage": "Gerir",
"marketing": "Marketing",
"member": "Membro",
"members": "Membros",
"members_and_teams": "Membros e equipas",
"membership_not_found": "Associação não encontrada",
@@ -383,6 +382,8 @@
"select_teams": "Selecionar equipas",
"selected": "Selecionado",
"selected_questions": "Perguntas selecionadas",
"selection": "Seleção",
"selections": "Seleções",
"send_test_email": "Enviar email de teste",
"session_not_found": "Sessão não encontrada",
"settings": "Configurações",
@@ -1755,6 +1756,7 @@
"welcome_message": "Mensagem de boas-vindas",
"without_a_filter_all_of_your_users_can_be_surveyed": "Sem um filtro, todos os seus utilizadores podem ser pesquisados.",
"you_have_not_created_a_segment_yet": "Ainda não criou um segmento",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Precisa de ter dois ou mais idiomas configurados no seu espaço de trabalho para trabalhar com traduções.",
"your_description_here_recall_information_with": "A sua descrição aqui. Recorde a informação com @",
"your_question_here_recall_information_with": "A sua pergunta aqui. Recorde a informação com @",
"your_web_app": "A sua aplicação web",
@@ -2028,7 +2030,6 @@
"starts": "Começa",
"starts_tooltip": "Número de vezes que o inquérito foi iniciado.",
"survey_reset_successfully": "Inquérito reiniciado com sucesso! {responseCount} respostas e {displayCount} exibições foram eliminadas.",
"survey_results": "Resultados de {surveyName}",
"this_month": "Este mês",
"this_quarter": "Este trimestre",
"this_year": "Este ano",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Copiază",
"copy_code": "Copiază codul",
"copy_link": "Copiază legătura",
"count_attributes": "{count, plural, one {{count} atribut} few {{count} atribute} other {{count} de atribute}}",
"count_contacts": "{count, plural, one {# contact} other {# contacte} }",
"count_members": "{count, plural, one {{count} membru} few {{count} membri} other {{count} de membri}}",
"count_responses": "{count, plural, one {# răspuns} other {# răspunsuri} }",
"count_selections": "{count, plural, one {{count} selecție} few {{count} selecții} other {{count} de selecții}}",
"count_attributes": "{value, plural, one {{value} atribut} few {{value} atribute} other {{value} de atribute}}",
"count_contacts": "{value, plural, one {# contact} other {# contacte} }",
"count_responses": "{value, plural, one {# răspuns} other {# răspunsuri} }",
"create_new_organization": "Creează organizație nouă",
"create_segment": "Creați segment",
"create_survey": "Creează sondaj",
@@ -276,6 +274,7 @@
"look_and_feel": "Aspect și Comportament",
"manage": "Gestionați",
"marketing": "Marketing",
"member": "Membru",
"members": "Membri",
"members_and_teams": "Membri și echipe",
"membership_not_found": "Apartenența nu a fost găsită",
@@ -383,6 +382,8 @@
"select_teams": "Selectați echipele",
"selected": "Selectat",
"selected_questions": "Întrebări selectate",
"selection": "Selecție",
"selections": "Selecții",
"send_test_email": "Trimite email de test",
"session_not_found": "Sesiune inexistentă",
"settings": "Setări",
@@ -1755,6 +1756,7 @@
"welcome_message": "Mesaj de bun venit",
"without_a_filter_all_of_your_users_can_be_surveyed": "Fără un filtru, toți utilizatorii pot fi chestionați.",
"you_have_not_created_a_segment_yet": "Nu ai creat încă un segment",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Trebuie să aveți cel puțin două limbi configurate în spațiul de lucru pentru a putea lucra cu traduceri.",
"your_description_here_recall_information_with": "Descrierea ta aici. Reamintiți informațiile cu @",
"your_question_here_recall_information_with": "Întrebarea ta aici. Reamintiți informațiile cu @",
"your_web_app": "Aplicația dumneavoastră web",
@@ -2028,7 +2030,6 @@
"starts": "Începuturi",
"starts_tooltip": "Număr de ori când sondajul a fost început.",
"survey_reset_successfully": "Resetarea chestionarului realizată cu succes! Au fost șterse {responseCount} răspunsuri și {displayCount} afișări.",
"survey_results": "Rezultatele {surveyName}",
"this_month": "Luna aceasta",
"this_quarter": "Trimestrul acesta",
"this_year": "Anul acesta",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Копировать",
"copy_code": "Скопировать код",
"copy_link": "Скопировать ссылку",
"count_attributes": "{count, plural, one {{count} атрибут} few {{count} атрибута} many {{count} атрибутов} other {{count} атрибута}}",
"count_contacts": "{count, plural, one {{count} контакт} few {{count} контакта} many {{count} контактов} other {{count} контактов}}",
"count_members": "{count, plural, one {{count} участник} few {{count} участника} many {{count} участников} other {{count} участника}}",
"count_responses": "{count, plural, one {{count} ответ} few {{count} ответа} many {{count} ответов} other {{count} ответов}}",
"count_selections": "{count, plural, one {{count} выбран} few {{count} выбрано} many {{count} выбрано} other {{count} выбрано}}",
"count_attributes": "{value, plural, one {{value} атрибут} few {{value} атрибута} many {{value} атрибутов} other {{value} атрибута}}",
"count_contacts": "{value, plural, one {{value} контакт} few {{value} контакта} many {{value} контактов} other {{value} контактов}}",
"count_responses": "{value, plural, one {{value} ответ} few {{value} ответа} many {{value} ответов} other {{value} ответов}}",
"create_new_organization": "Создать новую организацию",
"create_segment": "Создать сегмент",
"create_survey": "Создать опрос",
@@ -276,6 +274,7 @@
"look_and_feel": "Внешний вид",
"manage": "Управление",
"marketing": "Маркетинг",
"member": "Участник",
"members": "Участники",
"members_and_teams": "Участники и команды",
"membership_not_found": "Участие не найдено",
@@ -383,6 +382,8 @@
"select_teams": "Выбрать команды",
"selected": "Выбрано",
"selected_questions": "Выбранные вопросы",
"selection": "Выбор",
"selections": "Выборы",
"send_test_email": "Отправить тестовое письмо",
"session_not_found": "Сессия не найдена",
"settings": "Настройки",
@@ -1755,6 +1756,7 @@
"welcome_message": "Приветственное сообщение",
"without_a_filter_all_of_your_users_can_be_surveyed": "Без фильтра все ваши пользователи могут быть опрошены.",
"you_have_not_created_a_segment_yet": "Вы ещё не создали сегмент",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Для работы с переводами необходимо настроить два или более языков в рабочем пространстве.",
"your_description_here_recall_information_with": "Ваша инструкция здесь. Вспомните информацию с помощью @",
"your_question_here_recall_information_with": "Ваш вопрос здесь. Вспомните информацию с помощью @",
"your_web_app": "Ваше веб-приложение",
@@ -2028,7 +2030,6 @@
"starts": "Запуски",
"starts_tooltip": "Количество запусков опроса.",
"survey_reset_successfully": "Опрос успешно сброшен! {responseCount} ответов и {displayCount} показов были удалены.",
"survey_results": "Результаты {surveyName}",
"this_month": "В этом месяце",
"this_quarter": "В этом квартале",
"this_year": "В этом году",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "Kopiera",
"copy_code": "Kopiera kod",
"copy_link": "Kopiera länk",
"count_attributes": "{count, plural, one {{count} attribut} other {{count} attribut}}",
"count_contacts": "{count, plural, one {{count} kontakt} other {{count} kontakter}}",
"count_members": "{count, plural, one {{count} medlem} other {{count} medlemmar}}",
"count_responses": "{count, plural, one {{count} svar} other {{count} svar}}",
"count_selections": "{count, plural, one {{count} val} other {{count} val}}",
"count_attributes": "{value, plural, one {{value} attribut} other {{value} attribut}}",
"count_contacts": "{value, plural, one {{value} kontakt} other {{value} kontakter}}",
"count_responses": "{value, plural, one {{value} svar} other {{value} svar}}",
"create_new_organization": "Skapa ny organisation",
"create_segment": "Skapa segment",
"create_survey": "Skapa enkät",
@@ -276,6 +274,7 @@
"look_and_feel": "Utseende",
"manage": "Hantera",
"marketing": "Marknadsföring",
"member": "Medlem",
"members": "Medlemmar",
"members_and_teams": "Medlemmar och team",
"membership_not_found": "Medlemskap hittades inte",
@@ -383,6 +382,8 @@
"select_teams": "Välj team",
"selected": "Vald",
"selected_questions": "Valda frågor",
"selection": "Urval",
"selections": "Urval",
"send_test_email": "Skicka testmeddelande",
"session_not_found": "Session hittades inte",
"settings": "Inställningar",
@@ -1755,6 +1756,7 @@
"welcome_message": "Välkomstmeddelande",
"without_a_filter_all_of_your_users_can_be_surveyed": "Utan ett filter kan alla dina användare enkäteras.",
"you_have_not_created_a_segment_yet": "Du har inte skapat ett segment ännu",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "Du måste ha två eller fler språk inställda i din arbetsyta för att kunna arbeta med översättningar.",
"your_description_here_recall_information_with": "Din beskrivning här. Återkalla information med @",
"your_question_here_recall_information_with": "Din fråga här. Återkalla information med @",
"your_web_app": "Din webbapp",
@@ -2028,7 +2030,6 @@
"starts": "Starter",
"starts_tooltip": "Antal gånger enkäten har startats.",
"survey_reset_successfully": "Enkät återställd! {responseCount} svar och {displayCount} visningar togs bort.",
"survey_results": "Resultat för {surveyName}",
"this_month": "Denna månad",
"this_quarter": "Detta kvartal",
"this_year": "Detta år",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "复制",
"copy_code": "复制 代码",
"copy_link": "复制 链接",
"count_attributes": "{count, plural, one {{count} 个属性} other {{count} 个属性}}",
"count_contacts": "{count, plural, other {{count} 联系人} }",
"count_members": "{count, plural, one {{count} 位成员} other {{count} 位成员}}",
"count_responses": "{count, plural, other {{count} 回复} }",
"count_selections": "{count, plural, other {已选择{count}项}}",
"count_attributes": "{value, plural, one {{value} 个属性} other {{value} 个属性}}",
"count_contacts": "{value, plural, other {{value} 联系人} }",
"count_responses": "{value, plural, other {{value} 回复} }",
"create_new_organization": "创建 新的 组织",
"create_segment": "创建 细分",
"create_survey": "创建 调查",
@@ -276,6 +274,7 @@
"look_and_feel": "外观 & 感觉",
"manage": "管理",
"marketing": "市场营销",
"member": "成员",
"members": "成员",
"members_and_teams": "成员和团队",
"membership_not_found": "未找到会员资格",
@@ -383,6 +382,8 @@
"select_teams": "选择 团队",
"selected": "已选择",
"selected_questions": "选择的问题",
"selection": "选择",
"selections": "选择",
"send_test_email": "发送 测试 电子邮件",
"session_not_found": "会话 未找到",
"settings": "设置",
@@ -1755,6 +1756,7 @@
"welcome_message": "欢迎 信息",
"without_a_filter_all_of_your_users_can_be_surveyed": "没有 过滤器 时 ,所有 用户 都可以 被 调查 。",
"you_have_not_created_a_segment_yet": "您 还没有 创建 段落",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "要使用翻译功能,您的工作区需设置两种或以上语言。",
"your_description_here_recall_information_with": "在此输入描述。 调用信息与 @",
"your_question_here_recall_information_with": "在此输入你的问题。 调用信息与 @",
"your_web_app": "您的 网页应用",
@@ -2028,7 +2030,6 @@
"starts": "开始",
"starts_tooltip": "调查 被 开始 的 次数",
"survey_reset_successfully": "调查已重置成功!{responseCount} 个 反馈 和 {displayCount} 个 显示 已删除。",
"survey_results": "{surveyName} 结果",
"this_month": "本月",
"this_quarter": "本季度",
"this_year": "今年",
+7 -6
View File
@@ -175,11 +175,9 @@
"copy": "複製",
"copy_code": "複製程式碼",
"copy_link": "複製連結",
"count_attributes": "{count, plural, one {{count} 個屬性} other {{count} 個屬性}}",
"count_contacts": "{count, plural, other {{count} 聯絡人} }",
"count_members": "{count, plural, one {{count} 位成員} other {{count} 位成員}}",
"count_responses": "{count, plural, other {{count} 回應} }",
"count_selections": "{count, plural, one {{count} 個選項} other {{count} 個選項}}",
"count_attributes": "{value, plural, one {{value} 個屬性} other {{value} 個屬性}}",
"count_contacts": "{value, plural, other {{value} 聯絡人} }",
"count_responses": "{value, plural, other {{value} 回應} }",
"create_new_organization": "建立新組織",
"create_segment": "建立區隔",
"create_survey": "建立問卷",
@@ -276,6 +274,7 @@
"look_and_feel": "外觀與風格",
"manage": "管理",
"marketing": "行銷",
"member": "成員",
"members": "成員",
"members_and_teams": "成員與團隊",
"membership_not_found": "找不到成員資格",
@@ -383,6 +382,8 @@
"select_teams": "選擇 團隊",
"selected": "已選取",
"selected_questions": "選取的問題",
"selection": "選取",
"selections": "選取",
"send_test_email": "發送測試電子郵件",
"session_not_found": "找不到工作階段",
"settings": "設定",
@@ -1755,6 +1756,7 @@
"welcome_message": "歡迎訊息",
"without_a_filter_all_of_your_users_can_be_surveyed": "如果沒有篩選器,則可以調查您的所有使用者。",
"you_have_not_created_a_segment_yet": "您尚未建立區隔",
"you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations": "您必須在工作區中設定兩種或以上語言,才能進行翻譯作業。",
"your_description_here_recall_information_with": "您的描述在這裡。使用 @ 回憶資訊",
"your_question_here_recall_information_with": "您的問題在這裡。使用 @ 回憶資訊",
"your_web_app": "您的 Web 應用程式",
@@ -2028,7 +2030,6 @@
"starts": "開始次數",
"starts_tooltip": "問卷已開始的次數。",
"survey_reset_successfully": "調查 重置 成功!{responseCount} 條回應和 {displayCount} 個顯示被刪除。",
"survey_results": "{surveyName} 結果",
"this_month": "本月",
"this_quarter": "本季",
"this_year": "今年",
@@ -42,7 +42,7 @@ export const SingleResponseCardBody = ({
return (
<span
key={index}
className="ml-0.5 mr-0.5 rounded-md border border-slate-200 bg-slate-50 px-1 py-0.5 text-sm first:ml-0">
className="mr-0.5 ml-0.5 rounded-md border border-slate-200 bg-slate-50 px-1 py-0.5 text-sm first:ml-0">
@{part}
</span>
);
@@ -15,7 +15,7 @@ export const EmailChangeWithoutVerificationSuccessPage = async () => {
}
return (
<div className="flex min-h-screen bg-gradient-radial from-slate-200 to-slate-50">
<div className="bg-gradient-radial flex min-h-screen from-slate-200 to-slate-50">
<FormWrapper>
<h1 className="leading-2 mb-4 text-center font-bold">
{t("auth.email-change.email_change_success")}
@@ -60,7 +60,7 @@ export const ForgotPasswordForm = () => {
onChange={(e) => field.onChange(e)}
autoComplete="email"
required
className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
</FormControl>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
@@ -1,10 +1,8 @@
import { Invite } from "@prisma/client";
import { TUserLocale } from "@formbricks/types/user";
export interface InviteWithCreator extends Pick<
Invite,
"id" | "expiresAt" | "organizationId" | "role" | "teamIds"
> {
export interface InviteWithCreator
extends Pick<Invite, "id" | "expiresAt" | "organizationId" | "role" | "teamIds"> {
creator: {
name: string | null;
email: string;
+1 -1
View File
@@ -24,7 +24,7 @@ export const AuthLayout = async ({ children }: { children: React.ReactNode }) =>
<Toaster />
<div className="min-h-screen bg-slate-50">
<div className="isolate bg-white">
<div className="flex min-h-screen bg-gradient-radial from-slate-200 to-slate-50">{children}</div>
<div className="bg-gradient-radial flex min-h-screen from-slate-200 to-slate-50">{children}</div>
</div>
</div>
</>
@@ -184,7 +184,7 @@ export const LoginForm = ({
value={field.value}
onChange={(email) => field.onChange(email)}
placeholder="work@email.com"
className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
@@ -207,7 +207,7 @@ export const LoginForm = ({
aria-label="password"
aria-required="true"
required
className="block w-full rounded-md border-slate-300 pr-8 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 pr-8 shadow-sm sm:text-sm"
value={field.value}
onChange={(password) => field.onChange(password)}
/>
@@ -221,7 +221,7 @@ export const LoginForm = ({
<div className="ml-1 text-right transition-all duration-500 ease-in-out">
<Link
href="/auth/forgot-password"
className="text-xs text-slate-500 hover:text-brand-dark">
className="hover:text-brand-dark text-xs text-slate-500">
{t("auth.login.forgot_your_password")}
</Link>
</div>
@@ -222,7 +222,7 @@ export const SignupForm = ({
placeholder="*******"
aria-placeholder="password"
required
className="block w-full rounded-md shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md shadow-sm sm:text-sm"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</div>
@@ -6,7 +6,7 @@ export const VerifyEmailChangePage = async ({ searchParams }) => {
const { token } = await searchParams;
return (
<div className="flex min-h-screen bg-gradient-radial from-slate-200 to-slate-50">
<div className="bg-gradient-radial flex min-h-screen from-slate-200 to-slate-50">
<FormWrapper>
<EmailChangeSignIn token={token} />
<BackToLoginButton />
@@ -138,7 +138,7 @@ export const PricingTable = ({
<div className="flex flex-col gap-8">
<div className="flex flex-col">
<div className="flex w-full">
<h2 className="mb-3 mr-2 inline-flex w-full text-2xl font-bold text-slate-700">
<h2 className="mr-2 mb-3 inline-flex w-full text-2xl font-bold text-slate-700">
{t("environments.settings.billing.current_plan")}:{" "}
<span className="capitalize">{organization.billing.plan}</span>
{cancellingOn && (
@@ -203,7 +203,7 @@ export const PricingTable = ({
<div
className={cn(
"relative mx-8 mb-8 flex flex-col gap-4",
peopleUnlimitedCheck && "mb-0 mt-4 flex-row pb-0"
peopleUnlimitedCheck && "mt-4 mb-0 flex-row pb-0"
)}>
<p className="text-md font-semibold text-slate-700">
{t("environments.settings.billing.monthly_identified_users")}
@@ -226,7 +226,7 @@ export const PricingTable = ({
<div
className={cn(
"relative mx-8 flex flex-col gap-4 pb-6",
projectsUnlimitedCheck && "mb-0 mt-4 flex-row pb-0"
projectsUnlimitedCheck && "mt-4 mb-0 flex-row pb-0"
)}>
<p className="text-md font-semibold text-slate-700">{t("common.workspaces")}</p>
{organization.billing.limits.projects && (
@@ -264,7 +264,7 @@ export const PricingTable = ({
</button>
<button
aria-pressed={planPeriod === "yearly"}
className={`flex-1 items-center whitespace-nowrap rounded-md py-0.5 pl-4 pr-2 text-center ${
className={`flex-1 items-center rounded-md py-0.5 pr-2 pl-4 text-center whitespace-nowrap ${
planPeriod === "yearly" ? "bg-slate-200 font-semibold" : "bg-transparent"
}`}
onClick={() => handleMonthlyToggle("yearly")}>
@@ -276,7 +276,7 @@ export const PricingTable = ({
</div>
<div className="relative mx-auto grid max-w-md grid-cols-1 gap-y-8 lg:mx-0 lg:-mb-14 lg:max-w-none lg:grid-cols-3">
<div
className="hidden lg:absolute lg:inset-x-px lg:bottom-0 lg:top-4 lg:block lg:rounded-xl lg:rounded-t-2xl lg:border lg:border-slate-200 lg:bg-slate-100 lg:pb-8 lg:ring-1 lg:ring-white/10"
className="hidden lg:absolute lg:inset-x-px lg:top-4 lg:bottom-0 lg:block lg:rounded-xl lg:rounded-t-2xl lg:border lg:border-slate-200 lg:bg-slate-100 lg:pb-8 lg:ring-1 lg:ring-white/10"
aria-hidden="true"
/>
{getCloudPricingData(t).plans.map((plan) => (
@@ -98,7 +98,7 @@ export const ActivityTimeline = ({
<button
type="button"
onClick={toggleSort}
className="flex items-center px-1 text-slate-800 hover:text-brand-dark">
className="hover:text-brand-dark flex items-center px-1 text-slate-800">
<ArrowDownUpIcon className="inline h-4 w-4" />
</button>
</div>
@@ -46,7 +46,7 @@ export const generateAttributeTableColumns = (
cell: ({ row }) => {
const description = row.original.description;
return description ? (
<div className={isExpanded ? "whitespace-normal break-words" : "truncate"}>
<div className={isExpanded ? "break-words whitespace-normal" : "truncate"}>
<HighlightedText value={description} searchValue={searchValue} />
</div>
) : (
@@ -132,7 +132,7 @@ export const UploadContactsAttributes = ({
return (
<>
<span className="overflow-hidden text-ellipsis font-medium text-slate-700">{csvColumn}</span>
<span className="overflow-hidden font-medium text-ellipsis text-slate-700">{csvColumn}</span>
<div className="flex items-center gap-2">
<UploadContactsAttributeCombobox
open={open}
+3 -11
View File
@@ -682,18 +682,10 @@ export const createContactsFromCSV = async (
environmentId,
};
const CHUNK_SIZE = 50;
const allResults: (TContact | null)[] = [];
const contactPromises = csvData.map((record) => processCsvRecord(record, processingContext));
for (let i = 0; i < csvData.length; i += CHUNK_SIZE) {
const chunk = csvData.slice(i, i + CHUNK_SIZE);
const chunkResults = await Promise.all(
chunk.map((record) => processCsvRecord(record, processingContext))
);
allResults.push(...chunkResults);
}
return { contacts: allResults.filter((contact): contact is TContact => contact !== null) };
const results = await Promise.all(contactPromises);
return { contacts: results.filter((contact): contact is TContact => contact !== null) };
} catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) {
throw new DatabaseError(error.message);
@@ -93,7 +93,7 @@ export const EditSegmentModal = ({
key={tab.title}
className={`mr-4 px-1 pb-3 focus:outline-none ${
activeTab === index
? "border-b-2 border-brand-dark font-semibold text-slate-900"
? "border-brand-dark border-b-2 font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700"
}`}
onClick={() => handleTabClick(index)}>
@@ -42,7 +42,7 @@ export const SegmentTableDataRow = ({
</div>
</div>
</div>
<div className="col-span-1 my-auto hidden whitespace-nowrap text-center text-sm text-slate-500 sm:block">
<div className="col-span-1 my-auto hidden text-center text-sm whitespace-nowrap text-slate-500 sm:block">
<div className="ph-no-capture text-slate-900">{surveys?.length}</div>
</div>
<div className="whitespace-wrap col-span-1 my-auto hidden text-center text-sm text-slate-500 sm:block">
@@ -52,7 +52,7 @@ export const SegmentTableDataRow = ({
}).replace("about", "")}
</div>
</div>
<div className="col-span-1 my-auto hidden whitespace-normal text-center text-sm text-slate-500 sm:block">
<div className="col-span-1 my-auto hidden text-center text-sm whitespace-normal text-slate-500 sm:block">
<div className="ph-no-capture text-slate-900">{format(createdAt, "do 'of' MMMM, yyyy")}</div>
</div>
</button>
@@ -176,7 +176,7 @@ export function TargetingCard({
asChild
className="h-full w-full cursor-pointer rounded-lg hover:bg-slate-50">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
<div className="flex items-center pr-5 pl-2">
<CheckIcon
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
strokeWidth={3}
@@ -249,7 +249,7 @@ export function EditLanguage({
))}
</>
) : (
<p className="text-sm italic text-slate-500">
<p className="text-sm text-slate-500 italic">
{t("environments.workspace.languages.no_language_found")}
</p>
)}
@@ -56,7 +56,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
const { t } = useTranslation();
const environmentId = localSurvey.environmentId;
const open = activeElementId === "multiLanguage";
const [isMultiLanguageActivated, setIsMultiLanguageActivated] = useState(localSurvey.languages.length > 0);
const [isMultiLanguageActivated, setIsMultiLanguageActivated] = useState(localSurvey.languages.length > 1);
const [confirmationModalInfo, setConfirmationModalInfo] = useState<ConfirmationModalProps>({
title: "",
open: false,
@@ -188,7 +188,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
<div
className={cn(
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
"flex w-10 items-center justify-center rounded-l-lg border-t border-b border-l group-aria-expanded:rounded-bl-none"
)}>
<p>
<Languages className="h-6 w-6 rounded-full bg-indigo-500 p-1 text-white" />
@@ -250,15 +250,19 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
/>
) : (
<>
{projectLanguages.length === 0 && (
<div className="mb-4 text-sm italic text-slate-500">
{t("environments.surveys.edit.no_languages_found_add_first_one_to_get_started")}
{projectLanguages.length <= 1 && (
<div className="mb-4 text-sm text-slate-500 italic">
{projectLanguages.length === 0
? t("environments.surveys.edit.no_languages_found_add_first_one_to_get_started")
: t(
"environments.surveys.edit.you_need_to_have_two_or_more_languages_set_up_in_your_workspace_to_work_with_translations"
)}
</div>
)}
{projectLanguages.length > 0 && (
{projectLanguages.length > 1 && (
<div className="space-y-6">
{isMultiLanguageAllowed && !isMultiLanguageActivated ? (
<div className="text-sm italic text-slate-500">
<div className="text-sm text-slate-500 italic">
{t("environments.surveys.edit.switch_multi_language_on_to_get_started")}
</div>
) : null}
@@ -272,7 +276,7 @@ export const MultiLanguageCard: FC<MultiLanguageCardProps> = ({
setConfirmationModalInfo={setConfirmationModalInfo}
locale={locale}
/>
{defaultLanguage && projectLanguages.length > 1 ? (
{defaultLanguage ? (
<SecondaryLanguageSelect
defaultLanguage={defaultLanguage}
localSurvey={localSurvey}
@@ -40,7 +40,7 @@ export const AccessTable = ({ teams }: AccessTableProps) => {
<TableRow key={team.id} className="border-slate-200 hover:bg-transparent">
<TableCell className="font-medium">{team.name}</TableCell>
<TableCell>
{t("common.count_members", { count: team.memberCount })}
{team.memberCount} {team.memberCount === 1 ? t("common.member") : t("common.members")}
</TableCell>
<TableCell>
<IdBadge id={team.id} />
@@ -98,7 +98,7 @@ export const TeamsTable = ({
<TableRow key={team.id} id={team.name} className="hover:bg-transparent">
<TableCell>{team.name}</TableCell>
<TableCell>
{t("common.count_members", { count: team.memberCount })}
{team.memberCount} {team.memberCount === 1 ? t("common.member") : t("common.members")}
</TableCell>
<TableCell>
<Badge
@@ -121,7 +121,7 @@ export const TeamsTable = ({
<TableRow key={team.id} id={team.name} className="hover:bg-transparent">
<TableCell>{team.name}</TableCell>
<TableCell>
{t("common.count_members", { count: team.memberCount })}
{team.memberCount} {team.memberCount === 1 ? t("common.member") : t("common.members")}
</TableCell>
<TableCell></TableCell>
<TableCell className="flex justify-end">
@@ -77,7 +77,7 @@ export const ConfirmPasswordForm = ({
required
onChange={(password) => field.onChange(password)}
value={field.value}
className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</FormItem>
@@ -109,7 +109,7 @@ export const DisableTwoFactorModal = ({ open, setOpen }: DisableTwoFactorModalPr
required
onChange={(password) => field.onChange(password)}
value={field.value}
className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
/>
{error?.message && <FormError className="text-left">{error.message}</FormError>}
</FormItem>
@@ -35,7 +35,7 @@ export const TwoFactorBackup = ({ form }: TwoFactorBackupProps) => {
id="totpBackup"
required
placeholder="XXXXX-XXXXX"
className="block w-full rounded-md border-slate-300 shadow-sm focus:border-brand-dark focus:ring-brand-dark sm:text-sm"
className="focus:border-brand-dark focus:ring-brand-dark block w-full rounded-md border-slate-300 shadow-sm sm:text-sm"
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
/>
@@ -206,7 +206,7 @@ export const EmailCustomizationSettings = ({
<div className="mb-10">
<Small>{t("environments.settings.general.logo_in_email_header")}</Small>
<div className="mb-6 mt-2 flex items-center gap-4">
<div className="mt-2 mb-6 flex items-center gap-4">
{logoUrl && (
<div className="flex flex-col gap-2">
<div className="flex w-max items-center justify-center rounded-lg border border-slate-200 px-4 py-2">
@@ -276,7 +276,7 @@ export const EmailCustomizationSettings = ({
</Button>
</div>
</div>
<div className="min-h-52 w-[446px] rounded-t-lg border border-slate-100 px-10 pb-4 pt-10 shadow-card-xl">
<div className="shadow-card-xl min-h-52 w-[446px] rounded-t-lg border border-slate-100 px-10 pt-10 pb-4">
<Image
data-testid="email-customization-preview-image"
src={logoUrl || fbLogoUrl}
@@ -304,7 +304,7 @@ export const EmailCustomizationSettings = ({
)}
{hasWhiteLabelPermission && isReadOnly && (
<Alert variant="warning" className="mb-6 mt-4">
<Alert variant="warning" className="mt-4 mb-6">
<AlertDescription>
{t("common.only_owners_managers_and_manage_access_members_can_perform_this_action")}
</AlertDescription>
@@ -70,7 +70,7 @@ export const WebhookModal = ({ open, setOpen, webhook, surveys, isReadOnly }: We
type="button"
className={`mr-4 px-1 pb-3 focus:outline-none ${
activeTab === index
? "border-b-2 border-brand-dark font-semibold text-slate-900"
? "border-brand-dark border-b-2 font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700"
}`}
onClick={() => handleTabClick(index)}>
@@ -134,7 +134,7 @@ export const EditAPIKeys = ({ organizationId, apiKeys, locale, isReadOnly, proje
return (
<div className="flex items-center justify-between gap-2">
<span className="whitespace-pre-line break-all">{apiKey}</span>
<span className="break-all whitespace-pre-line">{apiKey}</span>
<div className="copyApiKeyIcon flex-shrink-0">
<FilesIcon
className="h-4 w-4 cursor-pointer"
@@ -162,7 +162,7 @@ export const EditAPIKeys = ({ organizationId, apiKeys, locale, isReadOnly, proje
</div>
<div className="grid-cols-9">
{apiKeysLocal?.length === 0 ? (
<div className="flex h-12 items-center justify-center whitespace-nowrap px-6 text-sm font-medium text-slate-400">
<div className="flex h-12 items-center justify-center px-6 text-sm font-medium whitespace-nowrap text-slate-400">
{t("environments.workspace.api_keys.no_api_keys_yet")}
</div>
) : (
@@ -10,7 +10,7 @@ const LoadingCard = () => {
return (
<div className="w-full max-w-4xl rounded-xl border border-slate-200 bg-white py-4 shadow-sm">
<div className="grid content-center border-b border-slate-200 px-4 pb-4 text-left text-slate-900">
<h3 className="h-6 w-full max-w-56 animate-pulse rounded-lg bg-slate-100 text-lg font-medium leading-6">
<h3 className="h-6 w-full max-w-56 animate-pulse rounded-lg bg-slate-100 text-lg leading-6 font-medium">
<span className="sr-only">{t("common.loading")}</span>
</h3>
<p className="mt-3 h-4 w-full max-w-80 animate-pulse rounded-lg bg-slate-100 text-sm text-slate-500">
@@ -50,10 +50,8 @@ export const TApiKeyEnvironmentPermission = z.object({
export type TApiKeyEnvironmentPermission = z.infer<typeof TApiKeyEnvironmentPermission>;
export interface TApiKeyWithEnvironmentPermission extends Pick<
ApiKey,
"id" | "label" | "createdAt" | "organizationAccess"
> {
export interface TApiKeyWithEnvironmentPermission
extends Pick<ApiKey, "id" | "label" | "createdAt" | "organizationAccess"> {
apiKeyEnvironments: TApiKeyEnvironmentPermission[];
}
@@ -106,23 +106,24 @@ export const OrganizationActions = ({
toast.error(errorMessage);
}
} else {
const inviteResults: { email: string; success: boolean }[] = [];
for (const { name, email, role, teamIds } of data) {
const inviteUserActionResult = await inviteUserAction({
organizationId: organization.id,
email: email.toLowerCase(),
name,
role,
teamIds,
});
inviteResults.push({
email,
success: Boolean(inviteUserActionResult?.data),
});
}
const failedInvites: string[] = [];
const successInvites: string[] = [];
inviteResults.forEach((invite) => {
const invitePromises = await Promise.all(
data.map(async ({ name, email, role, teamIds }) => {
const inviteUserActionResult = await inviteUserAction({
organizationId: organization.id,
email: email.toLowerCase(),
name,
role,
teamIds,
});
return {
email,
success: Boolean(inviteUserActionResult?.data),
};
})
);
let failedInvites: string[] = [];
let successInvites: string[] = [];
invitePromises.forEach((invite) => {
if (!invite.success) {
failedInvites.push(invite.email);
} else {
@@ -3,10 +3,8 @@ import { z } from "zod";
import { ZInvite } from "@formbricks/database/zod/invites";
import { ZUserName } from "@formbricks/types/user";
export interface TInvite extends Omit<
Invite,
"deprecatedRole" | "organizationId" | "creatorId" | "acceptorId" | "teamIds"
> {}
export interface TInvite
extends Omit<Invite, "deprecatedRole" | "organizationId" | "creatorId" | "acceptorId" | "teamIds"> {}
export interface InviteWithCreator extends Pick<Invite, "email"> {
creator: {
@@ -151,7 +151,7 @@ export const ActionActivityTab = ({
<Label className="block text-xs font-normal text-slate-500">Type</Label>
<div className="mt-1 flex items-center">
<div className="mr-1.5 h-4 w-4 text-slate-600">{ACTION_TYPE_ICON_LOOKUP[actionClass.type]}</div>
<p className="text-sm capitalize text-slate-700">{actionClass.type}</p>
<p className="text-sm text-slate-700 capitalize">{actionClass.type}</p>
</div>
</div>
<div className="">
@@ -90,7 +90,7 @@ export const CustomScriptsForm: React.FC<CustomScriptsFormProps> = ({ project, i
rows={8}
placeholder={t("environments.workspace.general.custom_scripts_placeholder")}
className={cn(
"flex w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:border-brand-dark focus:outline-none focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"focus:border-brand-dark flex w-full rounded-md border border-slate-300 bg-transparent px-3 py-2 font-mono text-xs text-slate-800 placeholder:text-slate-400 focus:ring-2 focus:ring-slate-400 focus:ring-offset-2 focus:outline-none disabled:cursor-not-allowed disabled:opacity-50",
isReadOnly && "bg-slate-50"
)}
{...field}
@@ -152,13 +152,13 @@ describe("tag lib", () => {
.mockResolvedValueOnce(baseTag as any)
.mockResolvedValueOnce(newTag as any);
vi.mocked(prisma.response.findMany).mockResolvedValueOnce([{ id: "resp1" }] as any);
vi.mocked(prisma.$transaction).mockResolvedValueOnce(undefined);
vi.mocked(prisma.$transaction).mockResolvedValueOnce(undefined).mockResolvedValueOnce(undefined);
const result = await mergeTags(baseTag.id, newTag.id);
expect(result).toEqual(ok(newTag));
expect(prisma.tag.findUnique).toHaveBeenCalledWith({ where: { id: baseTag.id } });
expect(prisma.tag.findUnique).toHaveBeenCalledWith({ where: { id: newTag.id } });
expect(prisma.response.findMany).toHaveBeenCalled();
expect(prisma.$transaction).toHaveBeenCalledTimes(1);
expect(prisma.$transaction).toHaveBeenCalledTimes(2);
});
test("merges tags with no responses with both tags", async () => {
vi.mocked(prisma.tag.findUnique)
@@ -195,20 +195,6 @@ describe("tag lib", () => {
});
}
});
test("returns error when merging a tag into itself", async () => {
const result = await mergeTags(baseTag.id, baseTag.id);
expect(result.ok).toBe(false);
if (!result.ok) {
expect(result.error).toStrictEqual({
code: "merge_same_tag",
message: "Cannot merge a tag into itself",
});
}
expect(prisma.tag.findUnique).not.toHaveBeenCalled();
expect(prisma.$transaction).not.toHaveBeenCalled();
});
test("throws on prisma error", async () => {
vi.mocked(prisma.tag.findUnique).mockRejectedValueOnce(new Error("fail"));
const result = await mergeTags(baseTag.id, newTag.id);
+74 -26
View File
@@ -72,13 +72,6 @@ export const mergeTags = async (
): Promise<Result<TTag, { code: TagError; message: string; meta?: Record<string, string> }>> => {
validateInputs([originalTagId, ZId], [newTagId, ZId]);
if (originalTagId === newTagId) {
return err({
code: TagError.MERGE_SAME_TAG,
message: "Cannot merge a tag into itself",
});
}
try {
let originalTag: TTag | null;
@@ -110,35 +103,90 @@ export const mergeTags = async (
});
}
// Find responses that have both tags to avoid unique constraint violations during merge
const responsesWithBothTags = await prisma.response.findMany({
// finds all the responses that have both the tags
let responsesWithBothTags = await prisma.response.findMany({
where: {
AND: [{ tags: { some: { tagId: originalTagId } } }, { tags: { some: { tagId: newTagId } } }],
AND: [
{
tags: {
some: {
tagId: {
in: [originalTagId],
},
},
},
},
{
tags: {
some: {
tagId: {
in: [newTagId],
},
},
},
},
],
},
select: { id: true },
});
const conflictResponseIds = responsesWithBothTags.map((r) => r.id);
await prisma.$transaction([
// Remove originalTag from responses that already have newTag (prevents unique constraint violation)
...(conflictResponseIds.length > 0
? [
if (!!responsesWithBothTags?.length) {
await Promise.all(
responsesWithBothTags.map(async (response) => {
await prisma.$transaction([
prisma.tagsOnResponses.deleteMany({
where: {
responseId: { in: conflictResponseIds },
tagId: originalTagId,
responseId: response.id,
tagId: {
in: [originalTagId, newTagId],
},
},
}),
]
: []),
// Move all remaining originalTag associations to newTag
prisma.tagsOnResponses.create({
data: {
responseId: response.id,
tagId: newTagId,
},
}),
]);
})
);
await prisma.$transaction([
prisma.tagsOnResponses.updateMany({
where: {
tagId: originalTagId,
},
data: {
tagId: newTagId,
},
}),
prisma.tag.delete({
where: {
id: originalTagId,
},
}),
]);
return ok(newTag);
}
await prisma.$transaction([
prisma.tagsOnResponses.updateMany({
where: { tagId: originalTagId },
data: { tagId: newTagId },
where: {
tagId: originalTagId,
},
data: {
tagId: newTagId,
},
}),
prisma.tag.delete({
where: {
id: originalTagId,
},
}),
// Delete the original tag
prisma.tag.delete({ where: { id: originalTagId } }),
]);
return ok(newTag);
@@ -203,9 +203,7 @@ export const ThemeStyling = ({
</FormDescription>
<FormControl>
<ColorPicker
color={
field.value ?? STYLE_DEFAULTS.brandColor?.light ?? COLOR_DEFAULTS.brandColor
}
color={field.value ?? STYLE_DEFAULTS.brandColor?.light ?? COLOR_DEFAULTS.brandColor}
onChange={(color) => field.onChange(color)}
containerClass="w-full"
/>
@@ -34,7 +34,7 @@ export const MergeTagsCombobox = ({ tags, onSelect }: MergeTagsComboboxProps) =>
<Button
variant="secondary"
size="sm"
className="font-medium text-slate-900 focus:border-transparent focus:shadow-transparent focus:outline-transparent focus:ring-0 focus:ring-transparent">
className="font-medium text-slate-900 focus:border-transparent focus:ring-0 focus:shadow-transparent focus:ring-transparent focus:outline-transparent">
{t("environments.workspace.tags.merge")}
</Button>
</PopoverTrigger>
@@ -43,7 +43,7 @@ export const MergeTagsCombobox = ({ tags, onSelect }: MergeTagsComboboxProps) =>
<div className="p-1">
<CommandInput
placeholder={t("environments.workspace.tags.search_tags")}
className="border-b border-none border-transparent shadow-none outline-0 ring-offset-transparent focus:border-none focus:border-transparent focus:shadow-none focus:outline-0 focus:ring-offset-transparent"
className="border-b border-none border-transparent shadow-none ring-offset-transparent outline-0 focus:border-none focus:border-transparent focus:shadow-none focus:ring-offset-transparent focus:outline-0"
/>
</div>
<CommandList className="border-0">
@@ -125,12 +125,12 @@ export const SingleTag: React.FC<SingleTagProps> = ({
</div>
</div>
<div className="col-span-1 my-auto whitespace-nowrap text-center text-sm text-slate-500">
<div className="col-span-1 my-auto text-center text-sm whitespace-nowrap text-slate-500">
<div className="text-slate-900">{tagCountLoading ? <LoadingSpinner /> : <p>{tagCount}</p>}</div>
</div>
{!isReadOnly && (
<div className="col-span-1 my-auto flex items-center justify-center gap-2 whitespace-nowrap text-center text-sm text-slate-500">
<div className="col-span-1 my-auto flex items-center justify-center gap-2 text-center text-sm whitespace-nowrap text-slate-500">
<div>
{isMergingTags ? (
<div className="w-24">
@@ -152,7 +152,7 @@ export const SingleTag: React.FC<SingleTagProps> = ({
<Button
variant="destructive"
size="sm"
className="font-medium text-slate-50 focus:border-transparent focus:shadow-transparent focus:outline-transparent focus:ring-0 focus:ring-transparent"
className="font-medium text-slate-50 focus:border-transparent focus:ring-0 focus:shadow-transparent focus:ring-transparent focus:outline-transparent"
onClick={() => setOpenDeleteTagDialog(true)}>
{t("common.delete")}
</Button>
@@ -1,6 +1,5 @@
export enum TagError {
TAG_NOT_FOUND = "tag_not_found",
TAG_NAME_ALREADY_EXISTS = "tag_name_already_exists",
MERGE_SAME_TAG = "merge_same_tag",
UNEXPECTED_ERROR = "unexpected_error",
}
@@ -391,7 +391,7 @@ export const ElementFormInput = ({
return (
<div className="w-full">
{label && (
<div className="mb-2 mt-3 flex items-center justify-between">
<div className="mt-3 mb-2 flex items-center justify-between">
<Label htmlFor={id}>{label}</Label>
{id === "headline" && currentElement && updateElement && (
<div className="flex items-center space-x-2">
@@ -521,7 +521,7 @@ export const ElementFormInput = ({
return (
<div className="w-full">
{label && (
<div className="mb-2 mt-3 flex items-center justify-between">
<div className="mt-3 mb-2 flex items-center justify-between">
<Label htmlFor={id}>{label}</Label>
</div>
)}
@@ -568,7 +568,7 @@ export const ElementFormInput = ({
<div className="h-10 w-full"></div>
<div
ref={highlightContainerRef}
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent ${
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll px-3 py-2 text-center text-sm whitespace-nowrap text-transparent ${
localSurvey.languages?.length > 1 ? "pr-24" : ""
}`}
dir="auto"
@@ -51,7 +51,7 @@ export const StartFromScratchTemplate = ({
const cardContent = (
<>
<PlusCircleIcon className="h-8 w-8 text-brand-dark transition-all duration-150 group-hover:scale-110" />
<PlusCircleIcon className="text-brand-dark h-8 w-8 transition-all duration-150 group-hover:scale-110" />
<h3 className="text-md mb-1 mt-3 text-left font-bold text-slate-700">{customSurvey.name}</h3>
<p className="text-left text-xs text-slate-600">{customSurvey.description}</p>
{showCreateSurveyButton && (
@@ -93,7 +93,7 @@ export const AddActionModal = ({
key={tab.title}
className={`mr-4 px-1 pb-3 focus:outline-none ${
activeTab === index
? "border-b-2 border-brand-dark font-semibold text-slate-900"
? "border-brand-dark border-b-2 font-semibold text-slate-900"
: "text-slate-500 hover:text-slate-700"
}`}
onClick={() => handleTabClick(index)}>
@@ -38,7 +38,7 @@ export const AddElementButton = ({ addElement, project, isCxMode }: AddElementBu
)}>
<Collapsible.CollapsibleTrigger asChild className="group h-full w-full">
<div className="inline-flex">
<div className="flex w-10 items-center justify-center rounded-l-lg bg-brand-dark group-aria-expanded:rounded-bl-none group-aria-expanded:rounded-br">
<div className="bg-brand-dark flex w-10 items-center justify-center rounded-l-lg group-aria-expanded:rounded-bl-none group-aria-expanded:rounded-br">
<PlusIcon className="h-5 w-5 text-white" />
</div>
<div className="px-4 py-3">
@@ -67,7 +67,7 @@ export const AddElementButton = ({ addElement, project, isCxMode }: AddElementBu
onMouseEnter={() => setHoveredElementId(elementType.id)}
onMouseLeave={() => setHoveredElementId(null)}>
<div className="flex items-center">
<elementType.icon className="-ml-0.5 mr-2 h-4 w-4 text-brand-dark" aria-hidden="true" />
<elementType.icon className="text-brand-dark -ml-0.5 mr-2 h-4 w-4" aria-hidden="true" />
{elementType.label}
</div>
<div
@@ -265,7 +265,7 @@ export const BlockCard = ({
</div>
<button
className="opacity-0 hover:cursor-move group-hover:opacity-100"
className="opacity-0 group-hover:opacity-100 hover:cursor-move"
aria-label="Drag to reorder block">
<GripIcon className="h-4 w-4" />
</button>
@@ -177,7 +177,7 @@ export const BulkEditOptionsModal = ({
}
}}
rows={15}
className="w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-sm focus:border-brand focus:outline-none"
className="focus:border-brand w-full rounded-md border border-slate-300 bg-white p-3 font-mono text-sm focus:outline-none"
placeholder={t("environments.surveys.edit.bulk_edit_description")}
/>
{validationError && <div className="text-sm text-red-600">{validationError}</div>}
@@ -111,7 +111,7 @@ export const CTAElementForm = ({
description={t("environments.surveys.edit.button_external_description")}
childBorder
customContainerClass="p-0 mt-4">
<div className="flex flex-1 flex-col gap-2 px-4 pb-4 pt-1">
<div className="flex flex-1 flex-col gap-2 px-4 pt-1 pb-4">
<ElementFormInput
id="ctaButtonLabel"
value={element.ctaButtonLabel}
@@ -185,7 +185,7 @@ export const EndScreenForm = ({
<div className="group relative">
{/* The highlight container is absolutely positioned behind the input */}
<div
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent`}
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll px-3 py-2 text-center text-sm whitespace-nowrap text-transparent`}
dir="auto"
key={highlightedJSX.toString()}>
{highlightedJSX}
@@ -172,7 +172,7 @@ export const FileUploadElementForm = ({
updateElement(elementIdx, { maxSizeInMB: Number.parseInt(e.target.value, 10) });
}}
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
className="mr-2 ml-2 inline w-20 bg-white text-center text-sm"
/>
MB
</p>
@@ -158,7 +158,7 @@ export const HiddenFieldsCard = ({
<div
className={cn(
open ? "bg-slate-50" : "bg-white group-hover:bg-slate-50",
"flex w-10 items-center justify-center rounded-l-lg border-b border-l border-t group-aria-expanded:rounded-bl-none"
"flex w-10 items-center justify-center rounded-l-lg border-t border-b border-l group-aria-expanded:rounded-bl-none"
)}>
<EyeOff className="h-4 w-4" />
</div>
@@ -191,7 +191,7 @@ export const HiddenFieldsCard = ({
);
})
) : (
<p className="mt-2 text-sm italic text-slate-500">
<p className="mt-2 text-sm text-slate-500 italic">
{t("environments.surveys.edit.no_hidden_fields_yet_add_first_one_below")}
</p>
)}
@@ -106,7 +106,7 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
className="h-full w-full cursor-pointer"
id="howToSendCardTrigger">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
<div className="flex items-center pr-5 pl-2">
<CheckIcon
strokeWidth={3}
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
@@ -149,14 +149,14 @@ export const HowToSendCard = ({ localSurvey, setLocalSurvey, environment }: HowT
option.comingSoon
? "border-slate-200 bg-slate-50/50"
: option.id === localSurvey.type
? "cursor-pointer border-brand-dark bg-slate-50"
? "border-brand-dark cursor-pointer bg-slate-50"
: "cursor-pointer bg-slate-50"
)}
id={`howToSendCardOption-${option.id}`}>
<RadioGroupItem
value={option.id}
id={option.id}
className="mx-5 disabled:border-slate-400 aria-checked:border-2 aria-checked:border-brand-dark"
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
disabled={option.comingSoon}
/>
<div className="inline-flex items-center">
@@ -124,7 +124,7 @@ export const LogoSettingsCard = ({
disabled && "cursor-not-allowed opacity-60 hover:bg-white"
)}>
<div className="inline-flex w-full px-4 py-4">
<div className="flex items-center pl-2 pr-5">
<div className="flex items-center pr-5 pl-2">
<CheckIcon
strokeWidth={3}
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
@@ -203,7 +203,7 @@ export const RecontactOptionsCard = ({ localSurvey, setLocalSurvey }: RecontactO
<RadioGroupItem
value={option.id}
id={`waiting-time-${option.id}`}
className="mx-5 disabled:border-slate-400 aria-checked:border-2 aria-checked:border-brand-dark"
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
/>
<div>
<p className="font-semibold text-slate-700">{option.name}</p>
@@ -273,7 +273,7 @@ export const RecontactOptionsCard = ({ localSurvey, setLocalSurvey }: RecontactO
<RadioGroupItem
value={option.id}
id={`recontact-option-${option.id}`}
className="mx-5 disabled:border-slate-400 aria-checked:border-2 aria-checked:border-brand-dark"
className="aria-checked:border-brand-dark mx-5 disabled:border-slate-400 aria-checked:border-2"
/>
<div>
<p className="font-semibold text-slate-700">{option.name}</p>
@@ -45,7 +45,7 @@ export const RedirectUrlForm = ({ localSurvey, endingCard, updateSurvey }: Redir
<div className="group relative">
{/* The highlight container is absolutely positioned behind the input */}
<div
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll whitespace-nowrap px-3 py-2 text-center text-sm text-transparent`}
className={`no-scrollbar absolute top-0 z-0 mt-0.5 flex h-10 w-full overflow-scroll px-3 py-2 text-center text-sm whitespace-nowrap text-transparent`}
dir="auto"
key={highlightedJSX.toString()}>
{highlightedJSX}
@@ -205,7 +205,7 @@ export const ResponseOptionsCard = ({
)}>
<Collapsible.CollapsibleTrigger asChild className="h-full w-full cursor-pointer">
<div className="inline-flex px-4 py-4">
<div className="flex items-center pl-2 pr-5">
<div className="flex items-center pr-5 pl-2">
<CheckIcon
strokeWidth={3}
className="h-7 w-7 rounded-full border border-green-300 bg-green-100 p-1.5 text-green-600"
@@ -243,7 +243,7 @@ export const ResponseOptionsCard = ({
value={localSurvey.autoComplete?.toString()}
onChange={handleInputResponse}
onBlur={handleInputResponseBlur}
className="ml-2 mr-2 inline w-20 bg-white text-center text-sm"
className="mr-2 ml-2 inline w-20 bg-white text-center text-sm"
/>
{t("environments.surveys.edit.completed_responses")}
</p>
@@ -310,7 +310,7 @@ export const ResponseOptionsCard = ({
<Input
autoFocus
id="heading"
className="mb-4 mt-2 bg-white"
className="mt-2 mb-4 bg-white"
name="heading"
defaultValue={surveyClosedMessage.heading}
onChange={(e) => handleClosedSurveyMessageChange({ heading: e.target.value })}
@@ -475,7 +475,7 @@ export const SurveyMenuBar = ({
/>
</div>
<div className="mt-3 flex items-center gap-2 sm:ml-4 sm:mt-0">
<div className="mt-3 flex items-center gap-2 sm:mt-0 sm:ml-4">
<AutoSaveIndicator isDraft={localSurvey.status === "draft"} lastSaved={lastAutoSaved} />
{!isStorageConfigured && (
<div>
@@ -155,7 +155,7 @@ export const FollowUpItem = ({
</div>
</button>
<div className="absolute right-4 top-4 flex items-center">
<div className="absolute top-4 right-4 flex items-center">
<TooltipRenderer tooltipContent={t("common.delete")}>
<Button
variant="ghost"

Some files were not shown because too many files have changed in this diff Show More