mirror of
https://github.com/formbricks/formbricks.git
synced 2026-01-31 12:23:33 -06:00
Compare commits
10 Commits
4.6.0-rc.1
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9573ae19e6 | ||
|
|
7b3f841c5e | ||
|
|
8f7d225d6a | ||
|
|
094b6dedba | ||
|
|
36f0be07c4 | ||
|
|
e079055a43 | ||
|
|
9ae9a3a9fc | ||
|
|
b4606c0113 | ||
|
|
6be654ab60 | ||
|
|
95c2e24416 |
@@ -23,7 +23,7 @@
|
||||
"@tailwindcss/vite": "4.1.18",
|
||||
"@typescript-eslint/parser": "8.53.0",
|
||||
"@vitejs/plugin-react": "5.1.2",
|
||||
"esbuild": "0.27.2",
|
||||
"esbuild": "0.25.12",
|
||||
"eslint-plugin-react-refresh": "0.4.26",
|
||||
"eslint-plugin-storybook": "10.1.11",
|
||||
"prop-types": "15.8.1",
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
import { CheckCircle2Icon } from "lucide-react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { TResponseWithQuotas } from "@formbricks/types/responses";
|
||||
import { TSurveyElementTypeEnum } from "@formbricks/types/surveys/constants";
|
||||
import { TSurvey } from "@formbricks/types/surveys/types";
|
||||
import { getTextContent } from "@formbricks/types/surveys/validation";
|
||||
import { getLocalizedValue } from "@/lib/i18n/utils";
|
||||
@@ -67,6 +68,16 @@ export const SingleResponseCardBody = ({
|
||||
<VerifiedEmail responseData={response.data} />
|
||||
)}
|
||||
{elements.map((question) => {
|
||||
// Skip CTA elements without external buttons only if they have no response data
|
||||
// This preserves historical data from when buttonExternal was true
|
||||
if (
|
||||
question.type === TSurveyElementTypeEnum.CTA &&
|
||||
!question.buttonExternal &&
|
||||
!response.data[question.id]
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const skipped = skippedQuestions.find((skippedQuestionElement) =>
|
||||
skippedQuestionElement.includes(question.id)
|
||||
);
|
||||
|
||||
@@ -425,11 +425,19 @@ export const SurveyMenuBar = ({
|
||||
const segment = await handleSegmentUpdate();
|
||||
clearSurveyLocalStorage();
|
||||
|
||||
await updateSurveyAction({
|
||||
const publishResult = await updateSurveyAction({
|
||||
...localSurvey,
|
||||
status,
|
||||
segment,
|
||||
});
|
||||
|
||||
if (!publishResult?.data) {
|
||||
const errorMessage = getFormattedErrorMessage(publishResult);
|
||||
toast.error(errorMessage);
|
||||
setIsSurveyPublishing(false);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSurveyPublishing(false);
|
||||
// Set flag to prevent beforeunload warning during navigation
|
||||
isSuccessfullySavedRef.current = true;
|
||||
@@ -467,7 +475,7 @@ export const SurveyMenuBar = ({
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex items-center gap-2 sm:mt-0 sm:ml-4">
|
||||
<div className="mt-3 flex items-center gap-2 sm:ml-4 sm:mt-0">
|
||||
<AutoSaveIndicator isDraft={localSurvey.status === "draft"} lastSaved={lastAutoSaved} />
|
||||
{!isStorageConfigured && (
|
||||
<div>
|
||||
|
||||
@@ -225,10 +225,10 @@ export const PreviewSurvey = ({
|
||||
)}>
|
||||
{previewMode === "mobile" && (
|
||||
<>
|
||||
<p className="absolute top-0 left-0 m-2 rounded bg-slate-100 px-2 py-1 text-xs text-slate-400">
|
||||
<p className="absolute left-0 top-0 m-2 rounded bg-slate-100 px-2 py-1 text-xs text-slate-400">
|
||||
Preview
|
||||
</p>
|
||||
<div className="absolute top-0 right-0 m-2">
|
||||
<div className="absolute right-0 top-0 m-2">
|
||||
<ResetProgressButton onClick={resetProgress} />
|
||||
</div>
|
||||
<MediaBackground
|
||||
@@ -259,12 +259,13 @@ export const PreviewSurvey = ({
|
||||
setBlockId = f;
|
||||
}}
|
||||
onFinished={onFinished}
|
||||
placement={placement}
|
||||
isSpamProtectionEnabled={isSpamProtectionEnabled}
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
<div className="flex h-full w-full flex-col justify-center px-1">
|
||||
<div className="absolute top-5 left-5">
|
||||
<div className="absolute left-5 top-5">
|
||||
{!styling.isLogoHidden && (
|
||||
<ClientLogo
|
||||
environmentId={environment.id}
|
||||
@@ -363,6 +364,7 @@ export const PreviewSurvey = ({
|
||||
}}
|
||||
onFinished={onFinished}
|
||||
isSpamProtectionEnabled={isSpamProtectionEnabled}
|
||||
placement={placement}
|
||||
/>
|
||||
</Modal>
|
||||
) : (
|
||||
@@ -371,7 +373,7 @@ export const PreviewSurvey = ({
|
||||
styling={styling}
|
||||
ContentRef={ContentRef as React.RefObject<HTMLDivElement>}
|
||||
isEditorView>
|
||||
<div className="absolute top-5 left-5">
|
||||
<div className="absolute left-5 top-5">
|
||||
{!styling.isLogoHidden && (
|
||||
<ClientLogo
|
||||
environmentId={environment.id}
|
||||
|
||||
2
apps/web/next-env.d.ts
vendored
2
apps/web/next-env.d.ts
vendored
@@ -1,6 +1,6 @@
|
||||
/// <reference types="next" />
|
||||
/// <reference types="next/image-types/global" />
|
||||
import "./.next/dev/types/routes.d.ts";
|
||||
import "./.next/types/routes.d.ts";
|
||||
|
||||
// NOTE: This file should not be edited
|
||||
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
|
||||
|
||||
@@ -97,11 +97,11 @@
|
||||
"jiti": "2.4.2",
|
||||
"jsonwebtoken": "9.0.2",
|
||||
"lexical": "0.36.2",
|
||||
"lodash": "4.17.21",
|
||||
"lodash": "4.17.23",
|
||||
"lucide-react": "0.507.0",
|
||||
"markdown-it": "14.1.0",
|
||||
"mime-types": "3.0.1",
|
||||
"next": "16.1.3",
|
||||
"next": "16.1.6",
|
||||
"next-auth": "4.24.12",
|
||||
"next-safe-action": "7.10.8",
|
||||
"node-fetch": "3.3.2",
|
||||
@@ -158,7 +158,7 @@
|
||||
"autoprefixer": "10.4.21",
|
||||
"cross-env": "10.0.0",
|
||||
"dotenv": "16.5.0",
|
||||
"esbuild": "0.25.11",
|
||||
"esbuild": "0.25.12",
|
||||
"postcss": "8.5.3",
|
||||
"resize-observer-polyfill": "1.5.1",
|
||||
"ts-node": "10.9.2",
|
||||
|
||||
@@ -47,8 +47,13 @@ run_with_timeout() {
|
||||
}
|
||||
|
||||
|
||||
echo "🗃️ Running database migrations..."
|
||||
run_with_timeout 300 "database migration" node packages/database/dist/scripts/apply-migrations.js
|
||||
# Check if migrations should be skipped (e.g., when using Helm migration job)
|
||||
if [ "${SKIP_STARTUP_MIGRATION:-false}" = "true" ]; then
|
||||
echo "⏭️ Skipping startup migrations (handled by migration job)"
|
||||
else
|
||||
echo "🗃️ Running database migrations..."
|
||||
run_with_timeout 300 "database migration" node packages/database/dist/scripts/apply-migrations.js
|
||||
fi
|
||||
|
||||
echo "🗃️ Running SAML database setup..."
|
||||
run_with_timeout 60 "SAML database setup" node packages/database/dist/scripts/create-saml-database.js
|
||||
|
||||
@@ -127,6 +127,10 @@ spec:
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- if .Values.migration.enabled }}
|
||||
- name: SKIP_STARTUP_MIGRATION
|
||||
value: "true"
|
||||
{{- end }}
|
||||
{{- range $key, $value := .Values.deployment.env }}
|
||||
- name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }}
|
||||
{{- if kindIs "string" $value }}
|
||||
@@ -1,7 +1,7 @@
|
||||
{{- if (.Values.externalSecret).enabled }}
|
||||
{{- range $nameSuffix, $data := .Values.externalSecret.files }}
|
||||
---
|
||||
apiVersion: external-secrets.io/v1beta1
|
||||
apiVersion: external-secrets.io/v1
|
||||
kind: ExternalSecret
|
||||
metadata:
|
||||
name: {{ template "formbricks.name" $ }}-{{ $nameSuffix }}
|
||||
95
charts/formbricks/templates/migration-job.yaml
Normal file
95
charts/formbricks/templates/migration-job.yaml
Normal file
@@ -0,0 +1,95 @@
|
||||
{{- if .Values.migration.enabled }}
|
||||
---
|
||||
apiVersion: batch/v1
|
||||
kind: Job
|
||||
metadata:
|
||||
name: {{ include "formbricks.name" . }}-migration
|
||||
labels:
|
||||
{{- include "formbricks.labels" . | nindent 4 }}
|
||||
annotations:
|
||||
# ArgoCD sync hooks
|
||||
argocd.argoproj.io/hook: PreSync
|
||||
argocd.argoproj.io/hook-delete-policy: HookSucceeded
|
||||
argocd.argoproj.io/sync-wave: "-1"
|
||||
{{- if .Values.migration.annotations }}
|
||||
{{- toYaml .Values.migration.annotations | nindent 4 }}
|
||||
{{- end }}
|
||||
spec:
|
||||
ttlSecondsAfterFinished: {{ .Values.migration.ttlSecondsAfterFinished | default 300 }}
|
||||
backoffLimit: {{ .Values.migration.backoffLimit | default 3 }}
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
{{- include "formbricks.selectorLabels" . | nindent 8 }}
|
||||
app.kubernetes.io/component: migration
|
||||
spec:
|
||||
restartPolicy: Never
|
||||
{{- if .Values.deployment.nodeSelector }}
|
||||
nodeSelector:
|
||||
{{- toYaml .Values.deployment.nodeSelector | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.deployment.tolerations }}
|
||||
tolerations:
|
||||
{{- toYaml .Values.deployment.tolerations | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.deployment.imagePullSecrets }}
|
||||
imagePullSecrets:
|
||||
{{- toYaml .Values.deployment.imagePullSecrets | nindent 8 }}
|
||||
{{- end }}
|
||||
{{- if .Values.rbac.serviceAccount.enabled }}
|
||||
serviceAccountName: {{ .Values.rbac.serviceAccount.name | default (include "formbricks.name" .) }}
|
||||
{{- end }}
|
||||
{{- if .Values.deployment.securityContext }}
|
||||
securityContext:
|
||||
{{- toYaml .Values.deployment.securityContext | nindent 8 }}
|
||||
{{- end }}
|
||||
containers:
|
||||
- name: migration
|
||||
image: {{ .Values.deployment.image.repository }}:{{ .Values.deployment.image.tag | default .Chart.AppVersion | default "latest" }}
|
||||
imagePullPolicy: {{ .Values.deployment.image.pullPolicy }}
|
||||
command:
|
||||
- node
|
||||
- packages/database/dist/scripts/apply-migrations.js
|
||||
{{- if or .Values.deployment.envFrom (or (and .Values.externalSecret.enabled (index .Values.externalSecret.files "app-secrets")) .Values.secret.enabled) }}
|
||||
envFrom:
|
||||
{{- if or .Values.secret.enabled (and .Values.externalSecret.enabled (index .Values.externalSecret.files "app-secrets")) }}
|
||||
- secretRef:
|
||||
name: {{ template "formbricks.name" . }}-app-secrets
|
||||
{{- end }}
|
||||
{{- range $value := .Values.deployment.envFrom }}
|
||||
{{- if (eq .type "configmap") }}
|
||||
- configMapRef:
|
||||
{{- if .name }}
|
||||
name: {{ include "formbricks.tplvalues.render" ( dict "value" $value.name "context" $ ) }}
|
||||
{{- else if .nameSuffix }}
|
||||
name: {{ template "formbricks.name" $ }}-{{ include "formbricks.tplvalues.render" ( dict "value" $value.nameSuffix "context" $ ) }}
|
||||
{{- else }}
|
||||
name: {{ template "formbricks.name" $ }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if (eq .type "secret") }}
|
||||
- secretRef:
|
||||
{{- if .name }}
|
||||
name: {{ include "formbricks.tplvalues.render" ( dict "value" $value.name "context" $ ) }}
|
||||
{{- else if .nameSuffix }}
|
||||
name: {{ template "formbricks.name" $ }}-{{ include "formbricks.tplvalues.render" ( dict "value" $value.nameSuffix "context" $ ) }}
|
||||
{{- else }}
|
||||
name: {{ template "formbricks.name" $ }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
env:
|
||||
{{- range $key, $value := .Values.deployment.env }}
|
||||
- name: {{ include "formbricks.tplvalues.render" ( dict "value" $key "context" $ ) }}
|
||||
{{- if kindIs "string" $value }}
|
||||
value: {{ include "formbricks.tplvalues.render" ( dict "value" $value "context" $ ) | quote }}
|
||||
{{- else }}
|
||||
{{- toYaml $value | nindent 14 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- if .Values.migration.resources }}
|
||||
resources:
|
||||
{{- toYaml .Values.migration.resources | nindent 12 }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
@@ -28,6 +28,32 @@ enterprise:
|
||||
enabled: false
|
||||
licenseKey: ""
|
||||
|
||||
##########################################################
|
||||
# Database Migration Job Configuration Helm
|
||||
##########################################################
|
||||
migration:
|
||||
# Enable migration job for ArgoCD deployments
|
||||
# When enabled, migrations run as a PreSync hook before the deployment
|
||||
# and the startup migration in the container is skipped
|
||||
enabled: true
|
||||
|
||||
# Additional annotations for the migration job
|
||||
annotations: {}
|
||||
|
||||
# Time to keep the job after completion (seconds)
|
||||
ttlSecondsAfterFinished: 300
|
||||
|
||||
# Number of retries before marking the job as failed
|
||||
backoffLimit: 3
|
||||
|
||||
# Resource requests and limits for the migration job
|
||||
resources:
|
||||
limits:
|
||||
memory: 512Mi
|
||||
requests:
|
||||
memory: 256Mi
|
||||
cpu: "100m"
|
||||
|
||||
##########################################################
|
||||
# Deployment Configuration
|
||||
##########################################################
|
||||
@@ -46,7 +46,7 @@
|
||||
"dependencies": {
|
||||
"react": "19.2.3",
|
||||
"react-dom": "19.2.3",
|
||||
"next": "16.1.3"
|
||||
"next": "16.1.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@azure/identity": "4.13.0",
|
||||
@@ -89,13 +89,14 @@
|
||||
"node-forge": ">=1.3.2",
|
||||
"tar-fs": "2.1.4",
|
||||
"typeorm": ">=0.3.26",
|
||||
"systeminformation": "5.27.14"
|
||||
"systeminformation": "5.27.14",
|
||||
"qs": ">=6.14.1"
|
||||
},
|
||||
"comments": {
|
||||
"overrides": "Security fixes for transitive dependencies. Remove when upstream packages update: axios (CVE-2025-58754) - awaiting @boxyhq/saml-jackson update | node-forge (Dependabot #230) - awaiting @boxyhq/saml-jackson update | tar-fs (Dependabot #205) - awaiting upstream dependency updates | typeorm (Dependabot #223) - awaiting @boxyhq/saml-jackson update | systeminformation (Dependabot #241) - awaiting @opentelemetry/host-metrics update"
|
||||
"overrides": "Security fixes for transitive dependencies. Remove when upstream packages update: axios (CVE-2025-58754) - awaiting @boxyhq/saml-jackson update | node-forge (Dependabot #230) - awaiting @boxyhq/saml-jackson update | tar-fs (Dependabot #205) - awaiting upstream dependency updates | typeorm (Dependabot #223) - awaiting @boxyhq/saml-jackson update | systeminformation (Dependabot #241) - awaiting @opentelemetry/host-metrics update | qs (Dependabot #245) - awaiting googleapis-common and stripe updates"
|
||||
},
|
||||
"patchedDependencies": {
|
||||
"next-auth@4.24.12": "patches/next-auth@4.24.12.patch"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -170,7 +170,7 @@ export const getLanguageCode = (survey: TEnvironmentStateSurvey, language?: stri
|
||||
|
||||
const selectedLanguage = survey.languages.find((surveyLanguage) => {
|
||||
return (
|
||||
surveyLanguage.language.code === language.toLowerCase() ||
|
||||
surveyLanguage.language.code.toLowerCase() === language.toLowerCase() ||
|
||||
surveyLanguage.language.alias?.toLowerCase() === language.toLowerCase()
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import * as React from "react";
|
||||
import { ElementError } from "@/components/general/element-error";
|
||||
import { ElementHeader } from "@/components/general/element-header";
|
||||
import { Label } from "@/components/general/label";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn, getRTLScaleOptionClasses } from "@/lib/utils";
|
||||
|
||||
interface NPSProps {
|
||||
/** Unique identifier for the element container */
|
||||
@@ -97,18 +97,9 @@ function NPS({
|
||||
const isLast = number === 10; // Last option is 10
|
||||
const isFirst = number === 0; // First option is 0
|
||||
|
||||
// Determine border radius and border classes
|
||||
// Use right border for all items to create separators, left border only on first item
|
||||
let borderRadiusClasses = "";
|
||||
let borderClasses = "border-t border-b border-r";
|
||||
|
||||
if (isFirst) {
|
||||
borderRadiusClasses = dir === "rtl" ? "rounded-r-input" : "rounded-l-input";
|
||||
borderClasses = "border-t border-b border-l border-r";
|
||||
} else if (isLast) {
|
||||
borderRadiusClasses = dir === "rtl" ? "rounded-l-input" : "rounded-r-input";
|
||||
// Last item keeps right border for rounded corner
|
||||
}
|
||||
// Use CSS logical properties for RTL-aware borders and border radius
|
||||
// The fieldset's dir attribute automatically handles direction
|
||||
const { borderRadiusClasses, borderClasses } = getRTLScaleOptionClasses(isFirst, isLast);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- label is interactive
|
||||
@@ -145,7 +136,7 @@ function NPS({
|
||||
setHoveredValue(null);
|
||||
}}>
|
||||
{colorCoding ? (
|
||||
<div className={cn("absolute top-0 left-0 h-[6px] w-full", getNPSOptionColor(number))} />
|
||||
<div className={cn("absolute left-0 top-0 h-[6px] w-full", getNPSOptionColor(number))} />
|
||||
) : null}
|
||||
<input
|
||||
type="radio"
|
||||
@@ -183,7 +174,7 @@ function NPS({
|
||||
{/* NPS Options */}
|
||||
<div className="relative">
|
||||
<ElementError errorMessage={errorMessage} dir={dir} />
|
||||
<fieldset className="w-full px-[2px]">
|
||||
<fieldset className="w-full px-[2px]" dir={dir}>
|
||||
<legend className="sr-only">NPS rating options</legend>
|
||||
<div className="flex w-full">{npsOptions.map((number) => renderNPSOption(number))}</div>
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
TiredFace,
|
||||
WearyFace,
|
||||
} from "@/components/general/smileys";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { cn, getRTLScaleOptionClasses } from "@/lib/utils";
|
||||
|
||||
/**
|
||||
* Get smiley color class based on range and index
|
||||
@@ -220,18 +220,9 @@ function Rating({
|
||||
const isLast = totalLength === number;
|
||||
const isFirst = number === 1;
|
||||
|
||||
// Determine border radius and border classes
|
||||
// Use right border for all items to create separators, left border only on first item
|
||||
let borderRadiusClasses = "";
|
||||
let borderClasses = "border-t border-b border-r";
|
||||
|
||||
if (isFirst) {
|
||||
borderRadiusClasses = dir === "rtl" ? "rounded-r-input" : "rounded-l-input";
|
||||
borderClasses = "border-t border-b border-l border-r";
|
||||
} else if (isLast) {
|
||||
borderRadiusClasses = dir === "rtl" ? "rounded-l-input" : "rounded-r-input";
|
||||
// Last item keeps right border for rounded corner
|
||||
}
|
||||
// Use CSS logical properties for RTL-aware borders and border radius
|
||||
// The parent div's dir attribute automatically handles direction
|
||||
const { borderRadiusClasses, borderClasses } = getRTLScaleOptionClasses(isFirst, isLast);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line jsx-a11y/no-noninteractive-element-interactions -- label is interactive
|
||||
@@ -269,7 +260,7 @@ function Rating({
|
||||
}}>
|
||||
{colorCoding ? (
|
||||
<div
|
||||
className={cn("absolute top-0 left-0 h-[6px] w-full", getRatingNumberOptionColor(range, number))}
|
||||
className={cn("absolute left-0 top-0 h-[6px] w-full", getRatingNumberOptionColor(range, number))}
|
||||
/>
|
||||
) : null}
|
||||
<input
|
||||
@@ -418,7 +409,7 @@ function Rating({
|
||||
{/* Rating Options */}
|
||||
<div className="relative">
|
||||
<ElementError errorMessage={errorMessage} dir={dir} />
|
||||
<fieldset className="w-full">
|
||||
<fieldset className="w-full" dir={dir}>
|
||||
<legend className="sr-only">Rating options</legend>
|
||||
<div className="flex w-full px-[2px]">
|
||||
{ratingOptions.map((number, index) => {
|
||||
|
||||
@@ -35,3 +35,29 @@ export const stripInlineStyles = (html: string): string => {
|
||||
KEEP_CONTENT: true,
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* Generate RTL-aware border radius and border classes for rating/NPS scale options
|
||||
* Uses CSS logical properties that automatically adapt to text direction
|
||||
* @param isFirst - Whether this is the first item in the scale
|
||||
* @param isLast - Whether this is the last item in the scale
|
||||
* @returns Object containing borderRadiusClasses and borderClasses
|
||||
*/
|
||||
export const getRTLScaleOptionClasses = (
|
||||
isFirst: boolean,
|
||||
isLast: boolean
|
||||
): { borderRadiusClasses: string; borderClasses: string } => {
|
||||
const borderRadiusClasses = cn(
|
||||
isFirst &&
|
||||
"[border-start-start-radius:var(--fb-input-border-radius)] [border-end-start-radius:var(--fb-input-border-radius)]",
|
||||
isLast &&
|
||||
"[border-start-end-radius:var(--fb-input-border-radius)] [border-end-end-radius:var(--fb-input-border-radius)]"
|
||||
);
|
||||
|
||||
const borderClasses = cn(
|
||||
"border-t border-b border-e", // block borders (top/bottom) and inline-end border
|
||||
isFirst && "border-s" // inline-start border for first item
|
||||
);
|
||||
|
||||
return { borderRadiusClasses, borderClasses };
|
||||
};
|
||||
|
||||
@@ -76,6 +76,7 @@ export function Survey({
|
||||
isSpamProtectionEnabled,
|
||||
dir = "auto",
|
||||
setDir,
|
||||
placement,
|
||||
}: SurveyContainerProps) {
|
||||
let apiClient: ApiClient | null = null;
|
||||
|
||||
@@ -743,7 +744,7 @@ export function Survey({
|
||||
return (
|
||||
<>
|
||||
{localSurvey.type !== "link" ? (
|
||||
<div className="bg-survey-bg flex h-6 justify-end pt-2 pr-2">
|
||||
<div className="bg-survey-bg flex h-6 justify-end pr-2 pt-2">
|
||||
<SurveyCloseButton onClose={onClose} />
|
||||
</div>
|
||||
) : null}
|
||||
@@ -916,6 +917,7 @@ export function Survey({
|
||||
setBlockId={setBlockId}
|
||||
shouldResetBlockId={shouldResetQuestionId}
|
||||
fullSizeCards={fullSizeCards}
|
||||
placement={placement}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ import { MutableRef } from "preact/hooks";
|
||||
import { useEffect, useMemo, useState } from "preact/hooks";
|
||||
import { JSX } from "preact/jsx-runtime";
|
||||
import React from "react";
|
||||
import { type TPlacement } from "@formbricks/types/common";
|
||||
import { TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
|
||||
@@ -17,6 +18,7 @@ interface StackedCardProps {
|
||||
cardWidth: number;
|
||||
hovered: boolean;
|
||||
cardArrangement: TCardArrangementOptions;
|
||||
placement: TPlacement;
|
||||
}
|
||||
|
||||
export const StackedCard = ({
|
||||
@@ -31,17 +33,24 @@ export const StackedCard = ({
|
||||
cardWidth,
|
||||
hovered,
|
||||
cardArrangement,
|
||||
placement,
|
||||
}: StackedCardProps) => {
|
||||
const isHidden = offset < 0;
|
||||
const [delayedOffset, setDelayedOffset] = useState<number>(offset);
|
||||
const [contentOpacity, setContentOpacity] = useState<number>(0);
|
||||
const currentCardHeight = offset === 0 ? "auto" : offset < 0 ? "initial" : cardHeight;
|
||||
|
||||
const getBottomStyles = () => {
|
||||
const getTopBottomStyles = () => {
|
||||
if (survey.type !== "link")
|
||||
return {
|
||||
bottom: 0,
|
||||
};
|
||||
if (placement === "bottomLeft" || placement === "bottomRight") {
|
||||
return {
|
||||
bottom: 0,
|
||||
};
|
||||
} else if (placement === "topLeft" || placement === "topRight") {
|
||||
return {
|
||||
top: 0,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const getDummyCardContent = () => {
|
||||
@@ -111,7 +120,7 @@ export const StackedCard = ({
|
||||
pointerEvents: offset === 0 ? "auto" : "none",
|
||||
...borderStyles,
|
||||
...straightCardArrangementStyles,
|
||||
...getBottomStyles(),
|
||||
...getTopBottomStyles(),
|
||||
}}
|
||||
className="pointer rounded-custom bg-survey-bg absolute inset-x-0 overflow-hidden">
|
||||
<div
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||||
import type { JSX } from "react";
|
||||
import { type TPlacement } from "@formbricks/types/common";
|
||||
import { type TJsEnvironmentStateSurvey } from "@formbricks/types/js";
|
||||
import { type TProjectStyling } from "@formbricks/types/project";
|
||||
import { type TCardArrangementOptions } from "@formbricks/types/styling";
|
||||
@@ -19,6 +20,7 @@ interface StackedCardsContainerProps {
|
||||
setBlockId: (blockId: string) => void;
|
||||
shouldResetBlockId?: boolean;
|
||||
fullSizeCards: boolean;
|
||||
placement?: TPlacement;
|
||||
}
|
||||
|
||||
export function StackedCardsContainer({
|
||||
@@ -30,6 +32,7 @@ export function StackedCardsContainer({
|
||||
setBlockId,
|
||||
shouldResetBlockId = true,
|
||||
fullSizeCards = false,
|
||||
placement = "bottomRight",
|
||||
}: Readonly<StackedCardsContainerProps>) {
|
||||
const [hovered, setHovered] = useState(false);
|
||||
const highlightBorderColor = survey.styling?.overwriteThemeStyling
|
||||
@@ -179,6 +182,7 @@ export function StackedCardsContainer({
|
||||
cardWidth={cardWidth}
|
||||
hovered={hovered}
|
||||
cardArrangement={cardArrangement}
|
||||
placement={placement}
|
||||
/>
|
||||
);
|
||||
})
|
||||
|
||||
@@ -36,18 +36,35 @@ export const renderSurvey = (props: SurveyContainerProps) => {
|
||||
throw new Error(`renderSurvey: Element with id ${containerId} not found.`);
|
||||
}
|
||||
|
||||
const { placement, darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props;
|
||||
// if survey type is link, we don't pass the placement, darkOverlay, clickOutside, onClose
|
||||
if (props.survey.type === "link") {
|
||||
const { placement, darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props;
|
||||
|
||||
render(
|
||||
h(
|
||||
I18nProvider,
|
||||
{ language },
|
||||
h(RenderSurvey, {
|
||||
...surveyInlineProps,
|
||||
})
|
||||
),
|
||||
element
|
||||
);
|
||||
render(
|
||||
h(
|
||||
I18nProvider,
|
||||
{ language },
|
||||
h(RenderSurvey, {
|
||||
...surveyInlineProps,
|
||||
})
|
||||
),
|
||||
element
|
||||
);
|
||||
} else {
|
||||
// For non-link surveys, pass placement through so it can be used in StackedCard
|
||||
const { darkOverlay, onClose, clickOutside, ...surveyInlineProps } = props;
|
||||
|
||||
render(
|
||||
h(
|
||||
I18nProvider,
|
||||
{ language },
|
||||
h(RenderSurvey, {
|
||||
...surveyInlineProps,
|
||||
})
|
||||
),
|
||||
element
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const modalContainer = document.createElement("div");
|
||||
modalContainer.id = "formbricks-modal-container";
|
||||
|
||||
460
pnpm-lock.yaml
generated
460
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user