From c68a9c8d1584a30ca5c4b70d005474d485dba93f Mon Sep 17 00:00:00 2001 From: Piyush Gupta Date: Tue, 18 Jul 2023 19:38:03 +0530 Subject: [PATCH 01/19] fix: edge case of close on date --- .../surveys/[surveyId]/SummaryHeader.tsx | 117 ++++++++++------- .../shared/SurveyStatusDropdown.tsx | 119 ++++++++++-------- 2 files changed, 139 insertions(+), 97 deletions(-) diff --git a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx index 174050afa1..bad9fbf51a 100644 --- a/apps/web/app/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx +++ b/apps/web/app/environments/[environmentId]/surveys/[surveyId]/SummaryHeader.tsx @@ -21,6 +21,10 @@ import { DropdownMenuSubTrigger, DropdownMenuTrigger, ErrorComponent, + Tooltip, + TooltipProvider, + TooltipTrigger, + TooltipContent, } from "@formbricks/ui"; import { CheckCircleIcon, @@ -48,6 +52,10 @@ const SummaryHeader = ({ surveyId, environmentId, survey }: SummaryHeaderProps) const { environment, isLoadingEnvironment, isErrorEnvironment } = useEnvironment(environmentId); const { triggerSurveyMutate } = useSurveyMutation(environmentId, surveyId); + const isCloseOnDateEnabled = survey.closeOnDate !== null; + const closeOnDate = survey.closeOnDate ? new Date(survey.closeOnDate) : null; + const isStatusChangeDisabled = (isCloseOnDateEnabled && closeOnDate && closeOnDate < new Date()) ?? false; + if (isLoadingProduct || isLoadingEnvironment) { return ; } @@ -64,53 +72,64 @@ const SummaryHeader = ({ surveyId, environmentId, survey }: SummaryHeaderProps)
{survey.type === "link" && } {(environment?.widgetSetupCompleted || survey.type === "link") && survey?.status !== "draft" ? ( - + + + + + + + To update the survey status, update the “Close +
survey on date” setting in the Response Options. +
+
+
) : null}
@@ -96,15 +96,15 @@ export default function SurveyStatusDropdown({ - Collect insights + In-progress - Pause Survey + Paused - Complete Survey + Completed diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 91c466e122..0abbcf7661 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.0' +lockfileVersion: '6.1' settings: autoInstallPeers: true @@ -19,7 +19,7 @@ importers: version: 3.12.7 turbo: specifier: latest - version: 1.10.7 + version: 1.10.3 apps/demo: dependencies: @@ -349,7 +349,7 @@ importers: version: 8.8.0(eslint@8.45.0) eslint-config-turbo: specifier: latest - version: 1.8.8(eslint@8.45.0) + version: 1.10.3(eslint@8.45.0) eslint-plugin-react: specifier: 7.32.2 version: 7.32.2(eslint@8.45.0) @@ -9573,13 +9573,13 @@ packages: eslint: 8.45.0 dev: true - /eslint-config-turbo@1.8.8(eslint@8.45.0): - resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==} + /eslint-config-turbo@1.10.3(eslint@8.45.0): + resolution: {integrity: sha512-ggzPfTJfMsMS383oZ4zfTP1zQvyMyiigOQJRUnLt1nqII6SKkTzdKZdwmXRDHU24KFwUfEFtT6c8vnm2VhL0uQ==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.45.0 - eslint-plugin-turbo: 1.8.8(eslint@8.45.0) + eslint-plugin-turbo: 1.10.3(eslint@8.45.0) dev: true /eslint-import-resolver-node@0.3.6: @@ -9767,8 +9767,8 @@ packages: semver: 6.3.1 string.prototype.matchall: 4.0.8 - /eslint-plugin-turbo@1.8.8(eslint@8.45.0): - resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==} + /eslint-plugin-turbo@1.10.3(eslint@8.45.0): + resolution: {integrity: sha512-g3Mnnk7el1FqxHfqbE/MayLvCsYjA/vKmAnUj66kV4AlM7p/EZqdt42NMcMSKtDVEm0w+utQkkzWG2Xsa0Pd/g==} peerDependencies: eslint: '>6.6.0' dependencies: @@ -19443,65 +19443,65 @@ packages: dependencies: safe-buffer: 5.2.1 - /turbo-darwin-64@1.10.7: - resolution: {integrity: sha512-N2MNuhwrl6g7vGuz4y3fFG2aR1oCs0UZ5HKl8KSTn/VC2y2YIuLGedQ3OVbo0TfEvygAlF3QGAAKKtOCmGPNKA==} + /turbo-darwin-64@1.10.3: + resolution: {integrity: sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.7: - resolution: {integrity: sha512-WbJkvjU+6qkngp7K4EsswOriO3xrNQag7YEGRtfLoDdMTk4O4QTeU6sfg2dKfDsBpTidTvEDwgIYJhYVGzrz9Q==} + /turbo-darwin-arm64@1.10.3: + resolution: {integrity: sha512-SBNmOZU9YEB0eyNIxeeQ+Wi0Ufd+nprEVp41rgUSRXEIpXjsDjyBnKnF+sQQj3+FLb4yyi/yZQckB+55qXWEsw==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.7: - resolution: {integrity: sha512-x1CF2CDP1pDz/J8/B2T0hnmmOQI2+y11JGIzNP0KtwxDM7rmeg3DDTtDM/9PwGqfPotN9iVGgMiMvBuMFbsLhg==} + /turbo-linux-64@1.10.3: + resolution: {integrity: sha512-kvAisGKE7xHJdyMxZLvg53zvHxjqPK1UVj4757PQqtx9dnjYHSc8epmivE6niPgDHon5YqImzArCjVZJYpIGHQ==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.7: - resolution: {integrity: sha512-JtnBmaBSYbs7peJPkXzXxsRGSGBmBEIb6/kC8RRmyvPAMyqF8wIex0pttsI+9plghREiGPtRWv/lfQEPRlXnNQ==} + /turbo-linux-arm64@1.10.3: + resolution: {integrity: sha512-Qgaqln0IYRgyL0SowJOi+PNxejv1I2xhzXOI+D+z4YHbgSx87ox1IsALYBlK8VRVYY8VCXl+PN12r1ioV09j7A==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.7: - resolution: {integrity: sha512-7A/4CByoHdolWS8dg3DPm99owfu1aY/W0V0+KxFd0o2JQMTQtoBgIMSvZesXaWM57z3OLsietFivDLQPuzE75w==} + /turbo-windows-64@1.10.3: + resolution: {integrity: sha512-rbH9wManURNN8mBnN/ZdkpUuTvyVVEMiUwFUX4GVE5qmV15iHtZfDLUSGGCP2UFBazHcpNHG1OJzgc55GFFrUw==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.7: - resolution: {integrity: sha512-D36K/3b6+hqm9IBAymnuVgyePktwQ+F0lSXr2B9JfAdFPBktSqGmp50JNC7pahxhnuCLj0Vdpe9RqfnJw5zATA==} + /turbo-windows-arm64@1.10.3: + resolution: {integrity: sha512-ThlkqxhcGZX39CaTjsHqJnqVe+WImjX13pmjnpChz6q5HHbeRxaJSFzgrHIOt0sUUVx90W/WrNRyoIt/aafniw==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.7: - resolution: {integrity: sha512-xm0MPM28TWx1e6TNC3wokfE5eaDqlfi0G24kmeHupDUZt5Wd0OzHFENEHMPqEaNKJ0I+AMObL6nbSZonZBV2HA==} + /turbo@1.10.3: + resolution: {integrity: sha512-U4gKCWcKgLcCjQd4Pl8KJdfEKumpyWbzRu75A6FCj6Ctea1PIm58W6Ltw1QXKqHrl2pF9e1raAskf/h6dlrPCA==} hasBin: true requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.10.7 - turbo-darwin-arm64: 1.10.7 - turbo-linux-64: 1.10.7 - turbo-linux-arm64: 1.10.7 - turbo-windows-64: 1.10.7 - turbo-windows-arm64: 1.10.7 + turbo-darwin-64: 1.10.3 + turbo-darwin-arm64: 1.10.3 + turbo-linux-64: 1.10.3 + turbo-linux-arm64: 1.10.3 + turbo-windows-64: 1.10.3 + turbo-windows-arm64: 1.10.3 dev: true /tween-functions@1.2.0: From f743fb18fb4b09f10f5832c1c8fb708008184121 Mon Sep 17 00:00:00 2001 From: Salim B Date: Mon, 24 Jul 2023 10:57:14 +0200 Subject: [PATCH 04/19] Fix Formatting in Docker Readme (#608) Fix formatting --- docker/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docker/README.md b/docker/README.md index fa0c5047ec..f9db6200c7 100644 --- a/docker/README.md +++ b/docker/README.md @@ -6,33 +6,33 @@ Follow the instructions below to quickly get Formbricks running on your system w Open a terminal and create a new directory for Formbricks, then navigate into this new directory: - \```bash + ```bash mkdir formbricks-quickstart && cd formbricks-quickstart - \``` + ``` 2. **Download the Docker-Compose File** Download the docker-compose file directly from the Formbricks repository: - \```bash + ```bash curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/docker/main/docker-compose.yml - \``` + ``` 3. **Generate NextAuth Secret** Next, you need to generate a NextAuth secret. This will be used for session signing and encryption. The `sed` command below generates a random string using `openssl`, then replaces the `NEXTAUTH_SECRET:` placeholder in the `docker-compose.yml` file with this generated secret: - \```bash + ```bash sed -i "/NEXTAUTH_SECRET:$/s/NEXTAUTH_SECRET:.\*/NEXTAUTH_SECRET: $(openssl rand -base64 32)/" docker-compose.yml - \``` + ``` 4. **Start the Docker Setup** You're now ready to start the Formbricks Docker setup. The following command will start Formbricks together with a postgreSQL database using Docker Compose: - \```bash + ```bash docker compose up -d - \``` + ``` The `-d` flag will run the containers in detached mode, meaning they'll run in the background. From 6b3f977d837549190c1ff50cf396f9200aa3bad9 Mon Sep 17 00:00:00 2001 From: Meet Patel Date: Mon, 24 Jul 2023 20:35:54 +0530 Subject: [PATCH 05/19] Store current Url in the responses meta data (#566) * url add to link-survey * fixed * fixed * fixed * fixed * ran pnpm format * make url optional in response input to not break existing integrations --------- Co-authored-by: Matthias Nannt --- apps/web/app/api/v1/client/responses/route.ts | 1 + apps/web/lib/linkSurvey/linkSurvey.ts | 3 +++ packages/js/src/components/SurveyView.tsx | 3 +++ packages/types/v1/responses.ts | 2 ++ 4 files changed, 9 insertions(+) diff --git a/apps/web/app/api/v1/client/responses/route.ts b/apps/web/app/api/v1/client/responses/route.ts index fac01954e7..152c496e76 100644 --- a/apps/web/app/api/v1/client/responses/route.ts +++ b/apps/web/app/api/v1/client/responses/route.ts @@ -45,6 +45,7 @@ export async function POST(request: Request): Promise { let response: TResponse; try { const meta = { + url: responseInput?.meta?.url ?? "", userAgent: { browser: agent?.browser.name, device: agent?.device.type, diff --git a/apps/web/lib/linkSurvey/linkSurvey.ts b/apps/web/lib/linkSurvey/linkSurvey.ts index 6569eea2dd..5e7bafef7e 100644 --- a/apps/web/lib/linkSurvey/linkSurvey.ts +++ b/apps/web/lib/linkSurvey/linkSurvey.ts @@ -108,6 +108,9 @@ export const useLinkSurveyUtils = (survey: Survey) => { personId: personId, finished, data, + meta: { + url: window.location.href, + }, }; if (!responseId && !isPreview) { const response = await createResponse( diff --git a/packages/js/src/components/SurveyView.tsx b/packages/js/src/components/SurveyView.tsx index 92404e81a2..88028b7caf 100644 --- a/packages/js/src/components/SurveyView.tsx +++ b/packages/js/src/components/SurveyView.tsx @@ -187,6 +187,9 @@ export default function SurveyView({ config, survey, close, errorHandler }: Surv personId: config.state.person.id, finished, data, + meta: { + url: window.location.href, + }, }; if (!responseId) { const [response, _] = await Promise.all([ diff --git a/packages/types/v1/responses.ts b/packages/types/v1/responses.ts index d62c10bada..3d2e2bc428 100644 --- a/packages/types/v1/responses.ts +++ b/packages/types/v1/responses.ts @@ -28,6 +28,7 @@ const ZResponseNote = z.object({ export type TResponseNote = z.infer; export const ZResponseMeta = z.object({ + url: z.string(), userAgent: z.object({ browser: z.string().optional(), os: z.string().optional(), @@ -67,6 +68,7 @@ export const ZResponseInput = z.object({ data: ZResponseData, meta: z .object({ + url: z.string().optional(), userAgent: z .object({ browser: z.string().optional(), From a165143c2a4b117dfc8584373aa5862b4cfea822 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Tue, 25 Jul 2023 14:57:09 +0530 Subject: [PATCH 06/19] Add Input Validation to the Survey Editor (#588) * added validation to survey edit * made refactors * extracted validation rules into a single object and resolved draggable issue * ran pnpm format * fixed a validation bug * fixed similar validation bug in other component * implemented default and specific validation * made some code refactors * handled case where a question is deleted * instead of storing questionIdx now we are storing question id in invalidQuestions array && ran pnpm format * removed unused comment * run pnpm format * made requested changes * removed unused export * add types to validation.ts * run pnpm format --------- Co-authored-by: Matthias Nannt --- .../[surveyId]/edit/CTAQuestionForm.tsx | 3 ++ .../[surveyId]/edit/ConsentQuestionForm.tsx | 4 +++ .../edit/MultipleChoiceMultiForm.tsx | 4 +++ .../edit/MultipleChoiceSingleForm.tsx | 4 +++ .../[surveyId]/edit/NPSQuestionForm.tsx | 3 ++ .../[surveyId]/edit/OpenQuestionForm.tsx | 3 ++ .../surveys/[surveyId]/edit/QuestionCard.tsx | 12 ++++++- .../surveys/[surveyId]/edit/QuestionsView.tsx | 31 ++++++++++++++++-- .../[surveyId]/edit/RatingQuestionForm.tsx | 3 ++ .../surveys/[surveyId]/edit/SurveyEditor.tsx | 5 ++- .../surveys/[surveyId]/edit/SurveyMenuBar.tsx | 32 +++++++++++++++++++ .../surveys/[surveyId]/edit/Validation.ts | 32 +++++++++++++++++++ packages/ui/components/Input.tsx | 4 ++- 13 files changed, 135 insertions(+), 5 deletions(-) create mode 100644 apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/Validation.ts diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/CTAQuestionForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/CTAQuestionForm.tsx index 7c58f0561c..46527bd8d7 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/CTAQuestionForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/CTAQuestionForm.tsx @@ -12,6 +12,7 @@ interface CTAQuestionFormProps { questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; lastQuestion: boolean; + isInValid: boolean; } export default function CTAQuestionForm({ @@ -19,6 +20,7 @@ export default function CTAQuestionForm({ questionIdx, updateQuestion, lastQuestion, + isInValid, }: CTAQuestionFormProps): JSX.Element { const [firstRender, setFirstRender] = useState(true); @@ -33,6 +35,7 @@ export default function CTAQuestionForm({ name="headline" value={question.headline} onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })} + isInvalid={isInValid && question.headline.trim() === ""} /> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ConsentQuestionForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ConsentQuestionForm.tsx index 42b13d4b26..a5dd247aea 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ConsentQuestionForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/ConsentQuestionForm.tsx @@ -11,12 +11,14 @@ interface ConsentQuestionFormProps { question: ConsentQuestion; questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; + isInValid: boolean; } export default function ConsentQuestionForm({ question, questionIdx, updateQuestion, + isInValid, }: ConsentQuestionFormProps): JSX.Element { const [firstRender, setFirstRender] = useState(true); return ( @@ -29,6 +31,7 @@ export default function ConsentQuestionForm({ name="headline" value={question.headline} onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })} + isInvalid={isInValid && question.headline.trim() === ""} /> @@ -62,6 +65,7 @@ export default function ConsentQuestionForm({ value={question.label} placeholder="I agree to the terms and conditions" onChange={(e) => updateQuestion(questionIdx, { label: e.target.value })} + isInvalid={isInValid && question.label.trim() === ""} /> {/*
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx index d9b583ae5e..5c0e99f7bf 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx @@ -21,12 +21,14 @@ interface OpenQuestionFormProps { questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; lastQuestion: boolean; + isInValid: boolean; } export default function MultipleChoiceMultiForm({ question, questionIdx, updateQuestion, + isInValid, }: OpenQuestionFormProps): JSX.Element { const lastChoiceRef = useRef(null); const [isNew, setIsNew] = useState(true); @@ -137,6 +139,7 @@ export default function MultipleChoiceMultiForm({ name="headline" value={question.headline} onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })} + isInvalid={isInValid && question.headline.trim() === ""} />
@@ -184,6 +187,7 @@ export default function MultipleChoiceMultiForm({ className={cn(choice.id === "other" && "border-dashed")} placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`} onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })} + isInvalid={isInValid && choice.label.trim() === ""} /> {question.choices && question.choices.length > 2 && ( void; lastQuestion: boolean; + isInValid: boolean; } export default function MultipleChoiceSingleForm({ question, questionIdx, updateQuestion, + isInValid, }: OpenQuestionFormProps): JSX.Element { const lastChoiceRef = useRef(null); const [isNew, setIsNew] = useState(true); @@ -137,6 +139,7 @@ export default function MultipleChoiceSingleForm({ name="headline" value={question.headline} onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })} + isInvalid={isInValid && question.headline.trim() === ""} /> @@ -184,6 +187,7 @@ export default function MultipleChoiceSingleForm({ className={cn(choice.id === "other" && "border-dashed")} placeholder={choice.id === "other" ? "Other" : `Option ${choiceIdx + 1}`} onChange={(e) => updateChoice(choiceIdx, { label: e.target.value })} + isInvalid={isInValid && choice.label.trim() === ""} /> {question.choices && question.choices.length > 2 && ( void; lastQuestion: boolean; + isInValid: boolean; } export default function NPSQuestionForm({ @@ -17,6 +18,7 @@ export default function NPSQuestionForm({ questionIdx, updateQuestion, lastQuestion, + isInValid, }: NPSQuestionFormProps): JSX.Element { const [showSubheader, setShowSubheader] = useState(!!question.subheader); @@ -31,6 +33,7 @@ export default function NPSQuestionForm({ name="headline" value={question.headline} onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })} + isInvalid={isInValid && question.headline.trim() === ""} /> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/OpenQuestionForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/OpenQuestionForm.tsx index 55c9ec0a3a..9206c23fad 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/OpenQuestionForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/OpenQuestionForm.tsx @@ -10,12 +10,14 @@ interface OpenQuestionFormProps { questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; lastQuestion: boolean; + isInValid: boolean; } export default function OpenQuestionForm({ question, questionIdx, updateQuestion, + isInValid, }: OpenQuestionFormProps): JSX.Element { const [showSubheader, setShowSubheader] = useState(!!question.subheader); @@ -30,6 +32,7 @@ export default function OpenQuestionForm({ name="headline" value={question.headline} onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })} + isInvalid={isInValid && question.headline.trim() === ""} /> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionCard.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionCard.tsx index d422231eef..e8c91eb372 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionCard.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionCard.tsx @@ -39,6 +39,7 @@ interface QuestionCardProps { activeQuestionId: string | null; setActiveQuestionId: (questionId: string | null) => void; lastQuestion: boolean; + isInValid: boolean; } export default function QuestionCard({ @@ -51,6 +52,7 @@ export default function QuestionCard({ activeQuestionId, setActiveQuestionId, lastQuestion, + isInValid, }: QuestionCardProps) { const question = localSurvey.questions[questionIdx]; const open = activeQuestionId === question.id; @@ -69,7 +71,8 @@ export default function QuestionCard({
{questionIdx + 1}
@@ -136,6 +139,7 @@ export default function QuestionCard({ questionIdx={questionIdx} updateQuestion={updateQuestion} lastQuestion={lastQuestion} + isInValid={isInValid} /> ) : question.type === QuestionType.MultipleChoiceSingle ? ( ) : question.type === QuestionType.MultipleChoiceMulti ? ( ) : question.type === QuestionType.NPS ? ( ) : question.type === QuestionType.CTA ? ( ) : question.type === QuestionType.Rating ? ( ) : question.type === "consent" ? ( ) : null}
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionsView.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionsView.tsx index f4927fc83c..30f38e9cd3 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionsView.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/QuestionsView.tsx @@ -9,6 +9,8 @@ import AddQuestionButton from "./AddQuestionButton"; import EditThankYouCard from "./EditThankYouCard"; import QuestionCard from "./QuestionCard"; import { StrictModeDroppable } from "./StrictModeDroppable"; +import { Question } from "@formbricks/types/questions"; +import { validateQuestion } from "./Validation"; interface QuestionsViewProps { localSurvey: Survey; @@ -16,6 +18,8 @@ interface QuestionsViewProps { activeQuestionId: string | null; setActiveQuestionId: (questionId: string | null) => void; environmentId: string; + invalidQuestions: String[] | null; + setInvalidQuestions: (invalidQuestions: String[] | null) => void; } export default function QuestionsView({ @@ -24,6 +28,8 @@ export default function QuestionsView({ localSurvey, setLocalSurvey, environmentId, + invalidQuestions, + setInvalidQuestions, }: QuestionsViewProps) { const internalQuestionIdMap = useMemo(() => { return localSurvey.questions.reduce((acc, question) => { @@ -44,12 +50,33 @@ export default function QuestionsView({ return survey; }; + // function to validate individual questions + const validateSurvey = (question: Question) => { + // prevent this function to execute further if user hasnt still tried to save the survey + if (invalidQuestions === null) { + return; + } + let temp = JSON.parse(JSON.stringify(invalidQuestions)); + if (validateQuestion(question)) { + temp = invalidQuestions.filter((id) => id !== question.id); + setInvalidQuestions(temp); + } else if (!invalidQuestions.includes(question.id)) { + temp.push(question.id); + setInvalidQuestions(temp); + } + }; + const updateQuestion = (questionIdx: number, updatedAttributes: any) => { let updatedSurvey = JSON.parse(JSON.stringify(localSurvey)); if ("id" in updatedAttributes) { // if the survey whose id is to be changed is linked to logic of any other survey then changing it const initialQuestionId = updatedSurvey.questions[questionIdx].id; updatedSurvey = handleQuestionLogicChange(updatedSurvey, initialQuestionId, updatedAttributes.id); + if (invalidQuestions?.includes(initialQuestionId)) { + setInvalidQuestions( + invalidQuestions.map((id) => (id === initialQuestionId ? updatedAttributes.id : id)) + ); + } // relink the question to internal Id internalQuestionIdMap[updatedAttributes.id] = @@ -63,6 +90,7 @@ export default function QuestionsView({ ...updatedAttributes, }; setLocalSurvey(updatedSurvey); + validateSurvey(updatedSurvey.questions[questionIdx]); }; const deleteQuestion = (questionIdx: number) => { @@ -120,7 +148,6 @@ export default function QuestionsView({ if (!result.destination) { return; } - const newQuestions = Array.from(localSurvey.questions); const [reorderedQuestion] = newQuestions.splice(result.source.index, 1); newQuestions.splice(result.destination.index, 0, reorderedQuestion); @@ -134,7 +161,6 @@ export default function QuestionsView({ const [reorderedQuestion] = newQuestions.splice(questionIndex, 1); const destinationIndex = up ? questionIndex - 1 : questionIndex + 1; newQuestions.splice(destinationIndex, 0, reorderedQuestion); - const updatedSurvey = { ...localSurvey, questions: newQuestions }; setLocalSurvey(updatedSurvey); }; @@ -159,6 +185,7 @@ export default function QuestionsView({ activeQuestionId={activeQuestionId} setActiveQuestionId={setActiveQuestionId} lastQuestion={questionIdx === localSurvey.questions.length - 1} + isInValid={invalidQuestions ? invalidQuestions.includes(question.id) : false} /> ))} {provided.placeholder} diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RatingQuestionForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RatingQuestionForm.tsx index 95e05466b5..94a708de14 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RatingQuestionForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/RatingQuestionForm.tsx @@ -12,6 +12,7 @@ interface RatingQuestionFormProps { questionIdx: number; updateQuestion: (questionIdx: number, updatedAttributes: any) => void; lastQuestion: boolean; + isInValid: boolean; } export default function RatingQuestionForm({ @@ -19,6 +20,7 @@ export default function RatingQuestionForm({ questionIdx, updateQuestion, lastQuestion, + isInValid, }: RatingQuestionFormProps) { const [showSubheader, setShowSubheader] = useState(!!question.subheader); @@ -33,6 +35,7 @@ export default function RatingQuestionForm({ name="headline" value={question.headline} onChange={(e) => updateQuestion(questionIdx, { headline: e.target.value })} + isInvalid={isInValid && question.headline.trim() === ""} />
diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx index d64bb83bf4..cb23359749 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/SurveyEditor.tsx @@ -22,7 +22,7 @@ export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorPr const [activeView, setActiveView] = useState<"questions" | "settings">("questions"); const [activeQuestionId, setActiveQuestionId] = useState(null); const [localSurvey, setLocalSurvey] = useState(); - + const [invalidQuestions, setInvalidQuestions] = useState(null); const { survey, isLoadingSurvey, isErrorSurvey } = useSurvey(environmentId, surveyId); const { product, isLoadingProduct, isErrorProduct } = useProduct(environmentId); const { environment, isLoadingEnvironment, isErrorEnvironment } = useEnvironment(environmentId); @@ -56,6 +56,7 @@ export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorPr environmentId={environmentId} activeId={activeView} setActiveId={setActiveView} + setInvalidQuestions={setInvalidQuestions} />
@@ -67,6 +68,8 @@ export default function SurveyEditor({ environmentId, surveyId }: SurveyEditorPr activeQuestionId={activeQuestionId} setActiveQuestionId={setActiveQuestionId} environmentId={environmentId} + invalidQuestions={invalidQuestions} + setInvalidQuestions={setInvalidQuestions} /> ) : ( void; + setInvalidQuestions: (invalidQuestions: String[]) => void; } export default function SurveyMenuBar({ @@ -30,6 +32,7 @@ export default function SurveyMenuBar({ setLocalSurvey, activeId, setActiveId, + setInvalidQuestions, }: SurveyMenuBarProps) { const router = useRouter(); const { triggerSurveyMutate, isMutatingSurvey } = useSurveyMutation(environmentId, localSurvey.id); @@ -37,6 +40,7 @@ export default function SurveyMenuBar({ const [isDeleteDialogOpen, setDeleteDialogOpen] = useState(false); const [isConfirmDialogOpen, setConfirmDialogOpen] = useState(false); const { product } = useProduct(environmentId); + let faultyQuestions: String[] = []; useEffect(() => { if (audiencePrompt && activeId === "settings") { @@ -85,6 +89,26 @@ export default function SurveyMenuBar({ } }; + const validateSurvey = (survey) => { + faultyQuestions = []; + for (let index = 0; index < survey.questions.length; index++) { + const question = survey.questions[index]; + const isValid = validateQuestion(question); + + if (!isValid) { + faultyQuestions.push(question.id); + } + } + // if there are any faulty questions, the user won't be allowed to save the survey + if (faultyQuestions.length > 0) { + setInvalidQuestions(faultyQuestions); + toast.error("Please fill required fields"); + return false; + } + + return true; + }; + const saveSurveyAction = (shouldNavigateBack = false) => { // variable named strippedSurvey that is a copy of localSurvey with isDraft removed from every question const strippedSurvey = { @@ -94,6 +118,11 @@ export default function SurveyMenuBar({ return rest; }), }; + + if (!validateSurvey(localSurvey)) { + return; + } + triggerSurveyMutate({ ...strippedSurvey }) .then(async (response) => { if (!response?.ok) { @@ -180,6 +209,9 @@ export default function SurveyMenuBar({ variant="darkCTA" loading={isMutatingSurvey} onClick={async () => { + if (!validateSurvey(localSurvey)) { + return; + } await triggerSurveyMutate({ ...localSurvey, status: "inProgress" }); router.push(`/environments/${environmentId}/surveys/${localSurvey.id}/summary?success=true`); }}> diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/Validation.ts b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/Validation.ts new file mode 100644 index 0000000000..b11dc523e0 --- /dev/null +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/Validation.ts @@ -0,0 +1,32 @@ +// extend this object in order to add more validation rules + +import { + MultipleChoiceMultiQuestion, + MultipleChoiceSingleQuestion, + Question, +} from "@formbricks/types/questions"; + +const validationRules = { + multipleChoiceMulti: (question: MultipleChoiceMultiQuestion) => { + return !question.choices.some((element) => element.label.trim() === ""); + }, + multipleChoiceSingle: (question: MultipleChoiceSingleQuestion) => { + return !question.choices.some((element) => element.label.trim() === ""); + }, + defaultValidation: (question: Question) => { + return question.headline.trim() !== ""; + }, +}; + +const validateQuestion = (question) => { + const specificValidation = validationRules[question.type]; + const defaultValidation = validationRules.defaultValidation; + + const specificValidationResult = specificValidation ? specificValidation(question) : true; + const defaultValidationResult = defaultValidation(question); + + // Return true only if both specific and default validation pass + return specificValidationResult && defaultValidationResult; +}; + +export { validateQuestion }; diff --git a/packages/ui/components/Input.tsx b/packages/ui/components/Input.tsx index 8032e72f1f..7f5cf2ac53 100644 --- a/packages/ui/components/Input.tsx +++ b/packages/ui/components/Input.tsx @@ -7,6 +7,7 @@ export interface InputProps dangerouslySetInnerHTML?: { __html: string; }; + isInvalid?: boolean; } const Input = React.forwardRef(({ className, ...props }, ref) => { @@ -14,7 +15,8 @@ const Input = React.forwardRef(({ className, ...pr Date: Tue, 25 Jul 2023 16:14:00 +0200 Subject: [PATCH 07/19] update pnpm lock --- pnpm-lock.yaml | 56 +++++++++++++++++++++++++------------------------- 1 file changed, 28 insertions(+), 28 deletions(-) diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0abbcf7661..91c466e122 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,4 +1,4 @@ -lockfileVersion: '6.1' +lockfileVersion: '6.0' settings: autoInstallPeers: true @@ -19,7 +19,7 @@ importers: version: 3.12.7 turbo: specifier: latest - version: 1.10.3 + version: 1.10.7 apps/demo: dependencies: @@ -349,7 +349,7 @@ importers: version: 8.8.0(eslint@8.45.0) eslint-config-turbo: specifier: latest - version: 1.10.3(eslint@8.45.0) + version: 1.8.8(eslint@8.45.0) eslint-plugin-react: specifier: 7.32.2 version: 7.32.2(eslint@8.45.0) @@ -9573,13 +9573,13 @@ packages: eslint: 8.45.0 dev: true - /eslint-config-turbo@1.10.3(eslint@8.45.0): - resolution: {integrity: sha512-ggzPfTJfMsMS383oZ4zfTP1zQvyMyiigOQJRUnLt1nqII6SKkTzdKZdwmXRDHU24KFwUfEFtT6c8vnm2VhL0uQ==} + /eslint-config-turbo@1.8.8(eslint@8.45.0): + resolution: {integrity: sha512-+yT22sHOT5iC1sbBXfLIdXfbZuiv9bAyOXsxTxFCWelTeFFnANqmuKB3x274CFvf7WRuZ/vYP/VMjzU9xnFnxA==} peerDependencies: eslint: '>6.6.0' dependencies: eslint: 8.45.0 - eslint-plugin-turbo: 1.10.3(eslint@8.45.0) + eslint-plugin-turbo: 1.8.8(eslint@8.45.0) dev: true /eslint-import-resolver-node@0.3.6: @@ -9767,8 +9767,8 @@ packages: semver: 6.3.1 string.prototype.matchall: 4.0.8 - /eslint-plugin-turbo@1.10.3(eslint@8.45.0): - resolution: {integrity: sha512-g3Mnnk7el1FqxHfqbE/MayLvCsYjA/vKmAnUj66kV4AlM7p/EZqdt42NMcMSKtDVEm0w+utQkkzWG2Xsa0Pd/g==} + /eslint-plugin-turbo@1.8.8(eslint@8.45.0): + resolution: {integrity: sha512-zqyTIvveOY4YU5jviDWw9GXHd4RiKmfEgwsjBrV/a965w0PpDwJgEUoSMB/C/dU310Sv9mF3DSdEjxjJLaw6rA==} peerDependencies: eslint: '>6.6.0' dependencies: @@ -19443,65 +19443,65 @@ packages: dependencies: safe-buffer: 5.2.1 - /turbo-darwin-64@1.10.3: - resolution: {integrity: sha512-IIB9IomJGyD3EdpSscm7Ip1xVWtYb7D0x7oH3vad3gjFcjHJzDz9xZ/iw/qItFEW+wGFcLSRPd+1BNnuLM8AsA==} + /turbo-darwin-64@1.10.7: + resolution: {integrity: sha512-N2MNuhwrl6g7vGuz4y3fFG2aR1oCs0UZ5HKl8KSTn/VC2y2YIuLGedQ3OVbo0TfEvygAlF3QGAAKKtOCmGPNKA==} cpu: [x64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-darwin-arm64@1.10.3: - resolution: {integrity: sha512-SBNmOZU9YEB0eyNIxeeQ+Wi0Ufd+nprEVp41rgUSRXEIpXjsDjyBnKnF+sQQj3+FLb4yyi/yZQckB+55qXWEsw==} + /turbo-darwin-arm64@1.10.7: + resolution: {integrity: sha512-WbJkvjU+6qkngp7K4EsswOriO3xrNQag7YEGRtfLoDdMTk4O4QTeU6sfg2dKfDsBpTidTvEDwgIYJhYVGzrz9Q==} cpu: [arm64] os: [darwin] requiresBuild: true dev: true optional: true - /turbo-linux-64@1.10.3: - resolution: {integrity: sha512-kvAisGKE7xHJdyMxZLvg53zvHxjqPK1UVj4757PQqtx9dnjYHSc8epmivE6niPgDHon5YqImzArCjVZJYpIGHQ==} + /turbo-linux-64@1.10.7: + resolution: {integrity: sha512-x1CF2CDP1pDz/J8/B2T0hnmmOQI2+y11JGIzNP0KtwxDM7rmeg3DDTtDM/9PwGqfPotN9iVGgMiMvBuMFbsLhg==} cpu: [x64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-linux-arm64@1.10.3: - resolution: {integrity: sha512-Qgaqln0IYRgyL0SowJOi+PNxejv1I2xhzXOI+D+z4YHbgSx87ox1IsALYBlK8VRVYY8VCXl+PN12r1ioV09j7A==} + /turbo-linux-arm64@1.10.7: + resolution: {integrity: sha512-JtnBmaBSYbs7peJPkXzXxsRGSGBmBEIb6/kC8RRmyvPAMyqF8wIex0pttsI+9plghREiGPtRWv/lfQEPRlXnNQ==} cpu: [arm64] os: [linux] requiresBuild: true dev: true optional: true - /turbo-windows-64@1.10.3: - resolution: {integrity: sha512-rbH9wManURNN8mBnN/ZdkpUuTvyVVEMiUwFUX4GVE5qmV15iHtZfDLUSGGCP2UFBazHcpNHG1OJzgc55GFFrUw==} + /turbo-windows-64@1.10.7: + resolution: {integrity: sha512-7A/4CByoHdolWS8dg3DPm99owfu1aY/W0V0+KxFd0o2JQMTQtoBgIMSvZesXaWM57z3OLsietFivDLQPuzE75w==} cpu: [x64] os: [win32] requiresBuild: true dev: true optional: true - /turbo-windows-arm64@1.10.3: - resolution: {integrity: sha512-ThlkqxhcGZX39CaTjsHqJnqVe+WImjX13pmjnpChz6q5HHbeRxaJSFzgrHIOt0sUUVx90W/WrNRyoIt/aafniw==} + /turbo-windows-arm64@1.10.7: + resolution: {integrity: sha512-D36K/3b6+hqm9IBAymnuVgyePktwQ+F0lSXr2B9JfAdFPBktSqGmp50JNC7pahxhnuCLj0Vdpe9RqfnJw5zATA==} cpu: [arm64] os: [win32] requiresBuild: true dev: true optional: true - /turbo@1.10.3: - resolution: {integrity: sha512-U4gKCWcKgLcCjQd4Pl8KJdfEKumpyWbzRu75A6FCj6Ctea1PIm58W6Ltw1QXKqHrl2pF9e1raAskf/h6dlrPCA==} + /turbo@1.10.7: + resolution: {integrity: sha512-xm0MPM28TWx1e6TNC3wokfE5eaDqlfi0G24kmeHupDUZt5Wd0OzHFENEHMPqEaNKJ0I+AMObL6nbSZonZBV2HA==} hasBin: true requiresBuild: true optionalDependencies: - turbo-darwin-64: 1.10.3 - turbo-darwin-arm64: 1.10.3 - turbo-linux-64: 1.10.3 - turbo-linux-arm64: 1.10.3 - turbo-windows-64: 1.10.3 - turbo-windows-arm64: 1.10.3 + turbo-darwin-64: 1.10.7 + turbo-darwin-arm64: 1.10.7 + turbo-linux-64: 1.10.7 + turbo-linux-arm64: 1.10.7 + turbo-windows-64: 1.10.7 + turbo-windows-arm64: 1.10.7 dev: true /tween-functions@1.2.0: From 469590c2f632acacd436b54776ccc73ee7385562 Mon Sep 17 00:00:00 2001 From: Shubham Palriwala Date: Wed, 26 Jul 2023 19:42:33 +0530 Subject: [PATCH 08/19] Add Quickstart for self-hosting using automated Shell Script (#613) * feat: poc * merge: individual docker-compose files * fix: nits * feat: postgres now only accessible internally * feat: emoji time * cleanup: my commented commands * better emoji and warning on domain name * fix: better handling of docker check * feat: follow principle of least privilege and remove excess sudo in commands * feat: read machine name dynamically * feat: documentation for prod script * feat: remove custom networks in the docker compose * cleanup: comments in script * update emojis to fix spacing * attempt: new groyp * attemp: move new group command at end for the ability to parse vars * feat: it all works without sudo yay * feat: cleanup docs as suggested * documentation: self hosting for prod script --------- Co-authored-by: Matthias Nannt --- .../docs/self-hosting/deployment/index.mdx | 62 +++++- docker/README.md | 48 ++--- docker/production.sh | 196 ++++++++++++++++++ 3 files changed, 268 insertions(+), 38 deletions(-) create mode 100644 docker/production.sh diff --git a/apps/formbricks-com/pages/docs/self-hosting/deployment/index.mdx b/apps/formbricks-com/pages/docs/self-hosting/deployment/index.mdx index 9dbd8a00a1..669ddd0c76 100644 --- a/apps/formbricks-com/pages/docs/self-hosting/deployment/index.mdx +++ b/apps/formbricks-com/pages/docs/self-hosting/deployment/index.mdx @@ -8,24 +8,56 @@ export const meta = { "Utilize Docker-Compose for easy deployment on your machine. Clone the repo, configure settings, and build the image to access the app on localhost.", }; -At Formbricks, we understand that different users have different needs, and we strive to cater to a wide variety of situations. This is why we provide two ways of running our application using Docker: +At Formbricks, we understand that different users have different needs, and we strive to cater to a wide variety of situations. This is why we provide three ways of running our application: -1. **Fast Setup with a Pre-built Docker Image:** This method is designed for those who want to quickly set up and start using Formbricks without getting into the technicalities of Docker or the build process. When you choose this method, you're using an image that we've already built for you. The pre-built image is ready-to-run, and it only requires minimal configuration on your part. This approach is perfect for getting a functional instance of Formbricks up and running with minimal hassle. It's as easy as downloading the Docker image and firing up the container. +1. **Production Instance Setup with Shell Script on Ubuntu**: If you want to quickly set up a production instance of Formbricks on a server running Ubuntu, we've got you covered! This method utilizes a convenient shell script that takes care of everything, including Docker, Postgres DB, and SSL certificate configuration. The shell script will automatically install all the required dependencies and configure your server, making the process a breeze. -2. **Manual Setup by Building the Docker Image from Source:** This approach provides the flexibility to configure every aspect of your Formbricks instance, including environment variables that need to be set at build time. While we don't recommend changing the source code of Formbricks, this method allows you to set your own configuration that might be necessary for specific deployment needs. Keep in mind that this method requires a more in-depth understanding of Docker and the build process. However, the trade-off is the additional control and flexibility you gain, making it worth considering if you're a more advanced user or have very specific configuration needs. +2. **Fast Setup with a Pre-built Docker Image:** This method is designed for those who want to quickly set up and start using Formbricks without getting into the technicalities of Docker or the build process. When you choose this method, you're using an image that we've already built for you. The pre-built image is ready-to-run, and it only requires minimal configuration on your part. This approach is perfect for getting a functional instance of Formbricks up and running with minimal hassle. It's as easy as downloading the Docker image and firing up the container. + +3. **Manual Setup by Building the Docker Image from Source:** This approach provides the flexibility to configure every aspect of your Formbricks instance, including environment variables that need to be set at build time. While we don't recommend changing the source code of Formbricks, this method allows you to set your own configuration that might be necessary for specific deployment needs. Keep in mind that this method requires a more in-depth understanding of Docker and the build process. However, the trade-off is the additional control and flexibility you gain, making it worth considering if you're a more advanced user or have very specific configuration needs. Please note that regardless of the method you choose, Formbricks is designed to be easy-to-use and flexible. So choose the method that best fits your comfort level and requirements, and start leveraging the power of Formbricks today! --- -## Requirements +## (Production: Ubuntu) Running the Shell Script + +This is the quickest way to get Formbricks up and running on an Ubuntu server. The shell script will automatically install all the required dependencies and configure your server, making the process a breeze. + +### Requirements + +Before you proceed, make sure you have the following: + +- A Linux Ubuntu Virtual Machine deployed with SSH access. + +- An A record set up to connect a custom domain to your instance. Formbricks will automatically create an SSL certificate for your domain using Let's Encrypt. + +## Single Command Setup + +Copy and paste the following command into your terminal: + +```bash +/bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/formbricks/formbricks/main/docker/production.sh)" +``` + +The script will prompt you for the following information: + +1. **Overwriting Docker GPG Keys**: If Docker GPG keys already exist, the script will ask if you want to overwrite them. + +2. **Email Address**: Provide your email address for SSL certificate registration with Let's Encrypt. + +3. **Domain Name**: Enter the domain name that Traefik will use to create the SSL certificate and forward requests to Formbricks. + +That's it! After running the command and providing the required information, visit the domain name you entered, and you should see the Formbricks home wizard! + +## (Most users: Local Setup) Running the pre-built Docker Image + +### Requirements Ensure `docker` & `docker compose` are installed on your server/system. Both are typically included with Docker utilities, like Docker Desktop and Rancher Desktop. **Note**: `docker compose` without the hyphen is now the primary method of using docker-compose, according to the Docker documentation. -## (Most users) Running the pre-built Docker Image - This is suitable for those who are testing Formbricks or running it with minimal to no modifications. For this we use the [public Docker image](https://hub.docker.com/r/formbricks/formbricks) and a simple docker-compose file. 1. **Create a New Directory for Formbricks** @@ -89,6 +121,12 @@ This is suitable for those who are testing Formbricks or running it with minimal ## (Advanced users) Build and Run Formbricks +### Requirements + +Ensure `docker` & `docker compose` are installed on your server/system. Both are typically included with Docker utilities, like Docker Desktop and Rancher Desktop. + +**Note**: `docker compose` without the hyphen is now the primary method of using docker-compose, according to the Docker documentation. + 1. Clone the repository: ```bash @@ -164,4 +202,16 @@ docker compose logs -f You can close the logs again with `CTRL + C`. +## Troubleshooting for the Shell Script Setup + +If you encounter any issues, consider the following steps: + +- **Inbound Rules**: Make sure you have added inbound rules for Port 80 and 443 in your VM's Security Group. + +- **A Record**: Verify that you have set up an A record for your domain, pointing to your VM's IP address. + +- **Check Docker Instances**: Run `docker ps` to check the status of the Docker instances. + +- **Check Formbricks Logs**: Run `cd formbricks && docker compose logs` to check the logs of the Formbricks stack. + export default ({ children }) => {children}; diff --git a/docker/README.md b/docker/README.md index f9db6200c7..c68b0fc39a 100644 --- a/docker/README.md +++ b/docker/README.md @@ -1,45 +1,29 @@ -# Formbricks Quickstart Using Docker +# Self Host Formbricks Production Instance -Follow the instructions below to quickly get Formbricks running on your system with Docker. This guide is designed for most users who want a straightforward setup process. +Follow this guide to get your Formbricks instance up and running with a Postgres DB and SSL certificate using a single script: -1. **Create a New Directory for Formbricks** +## Requirements - Open a terminal and create a new directory for Formbricks, then navigate into this new directory: +Before you proceed, make sure you have the following: - ```bash - mkdir formbricks-quickstart && cd formbricks-quickstart - ``` +- A Linux Ubuntu Virtual Machine deployed with SSH access. -2. **Download the Docker-Compose File** +- An A record set up to connect a custom domain to your instance. Formbricks will automatically create an SSL certificate for your domain using Let's Encrypt. - Download the docker-compose file directly from the Formbricks repository: +## Single Command Setup - ```bash - curl -o docker-compose.yml https://raw.githubusercontent.com/formbricks/formbricks/docker/main/docker-compose.yml - ``` +Copy and paste the following command into your terminal: -3. **Generate NextAuth Secret** +```bash +/bin/sh -c "$(curl -fsSL https://raw.githubusercontent.com/formbricks/formbricks/main/docker/production.sh)" +``` - Next, you need to generate a NextAuth secret. This will be used for session signing and encryption. The `sed` command below generates a random string using `openssl`, then replaces the `NEXTAUTH_SECRET:` placeholder in the `docker-compose.yml` file with this generated secret: +The script will prompt you for the following information: - ```bash - sed -i "/NEXTAUTH_SECRET:$/s/NEXTAUTH_SECRET:.\*/NEXTAUTH_SECRET: $(openssl rand -base64 32)/" docker-compose.yml - ``` +1. **Overwriting Docker GPG Keys**: If Docker GPG keys already exist, the script will ask if you want to overwrite them. -4. **Start the Docker Setup** +2. **Email Address**: Provide your email address for SSL certificate registration with Let's Encrypt. - You're now ready to start the Formbricks Docker setup. The following command will start Formbricks together with a postgreSQL database using Docker Compose: +3. **Domain Name**: Enter the domain name that Traefik will use to create the SSL certificate and forward requests to Formbricks. - ```bash - docker compose up -d - ``` - - The `-d` flag will run the containers in detached mode, meaning they'll run in the background. - -5. **Visit Formbricks in Your Browser** - - After starting the Docker setup, visit http://localhost:3000 in your browser to interact with the Formbricks application. The first time you access this page, you'll be greeted by a setup wizard. Follow the prompts to define your first user and get started. - -Enjoy using Formbricks! - -Note: For detailed documentation of local setup, take a look at our [self hosting docs](https://formbricks.com/docs/self-hosting/deployment) +That's it! After running the command and providing the required information, visit the domain name you entered, and you should see the Formbricks home wizard! diff --git a/docker/production.sh b/docker/production.sh new file mode 100644 index 0000000000..d3df81ec47 --- /dev/null +++ b/docker/production.sh @@ -0,0 +1,196 @@ +#!/bin/env bash + +set -e +ubuntu_version=$(lsb_release -a 2>/dev/null | grep -v "No LSB modules are available." | grep "Description:" | awk -F "Description:\t" '{print $2}') + +# Friendly welcome +echo "🧱 Welcome to the Formbricks single instance installer" +echo "" +echo "🛸 Fasten your seatbelts! We're setting up your Formbricks environment on your $ubuntu_version server." +echo "" + +# Remove any old Docker installations, without stopping the script if they're not found +echo "🧹 Time to sweep away any old Docker installations." +sudo apt-get remove docker docker-engine docker.io containerd runc >/dev/null 2>&1 || true + +# Update package list +echo "🔄 Updating your package list." +sudo apt-get update >/dev/null 2>&1 + +# Install dependencies +echo "📦 Installing the necessary dependencies." +sudo apt-get install -y \ + ca-certificates \ + curl \ + gnupg \ + lsb-release >/dev/null 2>&1 + +# Set up Docker's official GPG key & stable repository +echo "🔑 Adding Docker's official GPG key and setting up the stable repository." +sudo mkdir -m 0755 -p /etc/apt/keyrings >/dev/null 2>&1 +curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg >/dev/null 2>&1 +echo \ + "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ + $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list >/dev/null 2>&1 + +# Update package list again +echo "🔄 Updating your package list again." +sudo apt-get update >/dev/null 2>&1 + +# Install Docker +echo "🐳 Installing Docker." +sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin >/dev/null 2>&1 + +# Test Docker installation +echo "🚀 Testing your Docker installation." +if docker --version >/dev/null 2>&1; then + echo "🎉 Docker is installed!" +else + echo "❌ Docker is not installed. Please install Docker before proceeding." + exit 1 +fi + +# Adding your user to the Docker group +echo "🐳 Adding your user to the Docker group to avoid using sudo with docker commands." +sudo groupadd docker >/dev/null 2>&1 || true +sudo usermod -aG docker $USER >/dev/null 2>&1 + +echo "🎉 Hooray! Docker is all set and ready to go. You're now ready to run your Formbricks instance!" + +# Installing Traefik +echo "🚗 Installing Traefik..." +mkdir -p formbricks && cd formbricks +echo "📁 Created Formbricks Quickstart directory at ./formbricks." + +# Ask the user for their email address +echo "💡 Please enter your email address for the SSL certificate:" +read email_address + +cat <traefik.yaml +entryPoints: + web: + address: ":80" + http: + redirections: + entryPoint: + to: websecure + scheme: https + permanent: true + websecure: + address: ":443" + http: + tls: + certResolver: default +providers: + docker: + watch: true + exposedByDefault: false +certificatesResolvers: + default: + acme: + email: $email_address + storage: acme.json + caServer: "https://acme-v01.api.letsencrypt.org/directory" + tlsChallenge: {} +EOT + +echo "💡 Created traefik.yaml file with your provided email address." + +touch acme.json +chmod 600 acme.json +echo "💡 Created acme.json file with correct permissions." + +# Ask the user for their email address +echo "🔗 Please enter your domain name for the SSL certificate (🚨 do NOT enter the protocol (http/https/etc)):" +read domain_name + +cat <docker-compose.yml +version: "3.3" +x-environment: &environment + environment: + ######################################################################## + # ------------ MANDATORY (CHANGE ACCORDING TO YOUR SETUP) ------------# + ######################################################################## + + # PostgreSQL DB for Formbricks to connect to + DATABASE_URL: "postgresql://postgres:postgres@postgres:5432/formbricks?schema=public" + + # Uncomment to enable a dedicated connection pool for Prisma using Prisma Data Proxy + # Cold boots will be faster and you'll be able to scale your DB independently of your app. + # @see https://www.prisma.io/docs/data-platform/data-proxy/use-data-proxy + # PRISMA_GENERATE_DATAPROXY=true + PRISMA_GENERATE_DATAPROXY: + + # NextJS Auth + # @see: https://next-auth.js.org/configuration/options#nextauth_secret + # You can use: $(openssl rand -base64 32) to generate one + NEXTAUTH_SECRET: + # Set this to your public-facing URL, e.g., https://example.com + # You do not need the NEXTAUTH_URL environment variable in Vercel. + NEXTAUTH_URL: "https://$domain_name" + +services: + postgres: + restart: always + image: postgres:15-alpine + volumes: + - postgres:/var/lib/postgresql/data + environment: + - POSTGRES_PASSWORD=postgres + + formbricks: + restart: always + image: formbricks/formbricks:latest + depends_on: + - postgres + labels: + - "traefik.enable=true" # Enable Traefik for this service + - "traefik.http.routers.formbricks.rule=Host(\`$domain_name\`)" # Replace your_domain_name with your actual domain or IP + - "traefik.http.routers.formbricks.entrypoints=websecure" # Use the websecure entrypoint (port 443 with TLS) + - "traefik.http.services.formbricks.loadbalancer.server.port=3000" # Forward traffic to Formbricks on port 3000 + + ports: + - 3000:3000 + <<: *environment + traefik: + image: "traefik:v2.7" + restart: always + container_name: "traefik" + depends_on: + - formbricks + ports: + - "80:80" + - "443:443" + - "8080:8080" + volumes: + - ./traefik.yaml:/traefik.yaml + - ./acme.json:/acme.json + - /var/run/docker.sock:/var/run/docker.sock:ro + +volumes: + postgres: + driver: local +EOT + +update_nextauth_secret() { + nextauth_secret=$(openssl rand -base64 32) + sed -i "/NEXTAUTH_SECRET:$/s/NEXTAUTH_SECRET:.\*/NEXTAUTH_SECRET: $nextauth_secret/" docker-compose.yml +} + +echo "🚙 Updating NEXTAUTH_SECRET in the Formbricks container..." +while true; do + if update_nextauth_secret; then + echo "🚗 NEXTAUTH_SECRET updated successfully!" + break + else + echo "🚧 Failed to update NEXTAUTH_SECRET. Retrying..." + fi +done + +newgrp docker << END + +docker compose up -d + +echo "🚨 Make sure you have set up the DNS records as well as inbound rules for the domain name and IP address of this instance." +echo "" +echo "🎉 All done! Check the status of Formbricks & Traefik with 'cd formbricks && sudo docker compose ps.'" From b62a344e54ff8003157dc48ad1a52e4a92a7a327 Mon Sep 17 00:00:00 2001 From: Dhruwang Jariwala <67850763+Dhruwang@users.noreply.github.com> Date: Fri, 28 Jul 2023 14:11:21 +0530 Subject: [PATCH 09/19] Update Webhook Documentation (#611) * added webhook payload docs * ran pnpm format * update data.id explanation, reformat --------- Co-authored-by: Matthias Nannt --- apps/formbricks-com/lib/docsNavigation.ts | 1 + .../webhook-api/webhook-payload/index.mdx | 69 +++++++++++++++++++ 2 files changed, 70 insertions(+) create mode 100644 apps/formbricks-com/pages/docs/webhook-api/webhook-payload/index.mdx diff --git a/apps/formbricks-com/lib/docsNavigation.ts b/apps/formbricks-com/lib/docsNavigation.ts index 604660a71f..e6a4d16f15 100644 --- a/apps/formbricks-com/lib/docsNavigation.ts +++ b/apps/formbricks-com/lib/docsNavigation.ts @@ -78,6 +78,7 @@ const navigation = [ { title: "Get Webhook", href: "/docs/webhook-api/get-webhook" }, { title: "Create Webhook", href: "/docs/webhook-api/create-webhook" }, { title: "Delete Webhook", href: "/docs/webhook-api/delete-webhook" }, + { title: "Webhook Payload", href: "/docs/webhook-api/webhook-payload" }, ], }, { diff --git a/apps/formbricks-com/pages/docs/webhook-api/webhook-payload/index.mdx b/apps/formbricks-com/pages/docs/webhook-api/webhook-payload/index.mdx new file mode 100644 index 0000000000..8203bdbc4a --- /dev/null +++ b/apps/formbricks-com/pages/docs/webhook-api/webhook-payload/index.mdx @@ -0,0 +1,69 @@ +import { Layout } from "@/components/docs/Layout"; +import { Fence } from "@/components/shared/Fence"; + +export const meta = { + title: "Webhook Payload", + description: "Learn how to use the Formbricks Webhook API.", +}; + +This documentation helps understand the payload structure that will be received when the webhook is triggered in Formbricks. + +## An example webhook payload + +``` +{ + "webhookId": "cljwxvjos0003qhnvj2jg4k5i", + "event": "responseCreated", + "data": { + "id": "cljwy2m8r0001qhclco1godnu", + "createdAt": "2023-07-10T14:14:17.115Z", + "updatedAt": "2023-07-10T14:14:17.115Z", + "surveyId": "cljsf3d7a000019cv9apt2t27", + "finished": false, + "data": { + "qumbk3fkr6cky8850bvvq5z1": "Executive" + }, + "meta": { + "userAgent": { + "os": "Mac OS", + "browser": "Chrome" + } + }, + "personAttributes": { + "email": "test@web.com", + "userId": "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING" + }, + "person": { + "id": "cljold01t0000qh8ewzigzmjk", + "attributes": { + "email": "test@web.com", + "userId": "THIS-IS-A-VERY-LONG-USER-ID-FOR-TESTING" + }, + "createdAt": "2023-07-04T17:56:17.154Z", + "updatedAt": "2023-07-04T17:56:17.154Z" + }, + "notes": [], + "tags": [] + } +} + +``` + +| Variable | Type | Description | +| --------------------- | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | +| webhookId | String | Webhook's Id | +| event | String | The name of the trigger event [responseCreated, responseUpdated, responseFinished] | +| data | Object | Contains the details of the newly created response. | +| data.id | String | Formbricks Response ID. | +| data.createdAt | String | The timestamp when the response was created. | +| data.updatedAt | String | The timestamp when the response was last updated. | +| data.surveyId | String | The identifier of the survey associated with this response. | +| data.finished | Boolean | A boolean value indicating whether the survey response is marked as finished. | +| data.data | Object | An object containing the response data, where keys are question identifiers, and values are the corresponding answers given by the respondent. | +| data.meta | Object | Additional metadata related to the response, such as the user's operating system and browser information. | +| data.personAttributes | Object | An object with attributes related to the respondent, such as their email and a user ID (if available). | +| data.person | Object | Information about the respondent, including their unique id, attributes, and creation/update timestamps. | +| data.notes | Array | An array of notes associated with the response (if any). | +| data.tags | Array | An array of tags assigned to the response (if any). | + +export default ({ children }) => {children}; From e864829a7972992d1e7adfbc4ca0e825890d0776 Mon Sep 17 00:00:00 2001 From: Moritz Rengert <42251569+moritzrengert@users.noreply.github.com> Date: Fri, 28 Jul 2023 20:21:26 +0200 Subject: [PATCH 10/19] Fix: Logic Jumps stop working when options get renamed (#540) * update logic values if multi select options change * run pnpm format --------- Co-authored-by: Matthias Nannt --- .../edit/MultipleChoiceMultiForm.tsx | 32 +++++++++++++------ .../edit/MultipleChoiceSingleForm.tsx | 32 +++++++++++++------ 2 files changed, 44 insertions(+), 20 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx index 5c0e99f7bf..e086541663 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceMultiForm.tsx @@ -53,16 +53,28 @@ export default function MultipleChoiceMultiForm({ }, }; - const updateChoice = (choiceIdx: number, updatedAttributes: any) => { - const newChoices = !question.choices - ? [] - : question.choices.map((choice, idx) => { - if (idx === choiceIdx) { - return { ...choice, ...updatedAttributes }; - } - return choice; - }); - updateQuestion(questionIdx, { choices: newChoices }); + const updateChoice = (choiceIdx: number, updatedAttributes: { label: string }) => { + const newLabel = updatedAttributes.label; + const oldLabel = question.choices[choiceIdx].label; + let newChoices: any[] = []; + if (question.choices) { + newChoices = question.choices.map((choice, idx) => { + if (idx !== choiceIdx) return choice; + return { ...choice, ...updatedAttributes }; + }); + } + + let newLogic: any[] = []; + question.logic?.forEach((logic) => { + let newL: string | string[] | undefined = logic.value; + if (Array.isArray(logic.value)) { + newL = logic.value.map((value) => (value === oldLabel ? newLabel : value)); + } else { + newL = logic.value === oldLabel ? newLabel : logic.value; + } + newLogic.push({ ...logic, value: newL }); + }); + updateQuestion(questionIdx, { choices: newChoices, logic: newLogic }); }; const addChoice = (choiceIdx?: number) => { diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx index a91188f36f..db5bf4a460 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/[surveyId]/edit/MultipleChoiceSingleForm.tsx @@ -53,16 +53,28 @@ export default function MultipleChoiceSingleForm({ }, }; - const updateChoice = (choiceIdx: number, updatedAttributes: any) => { - const newChoices = !question.choices - ? [] - : question.choices.map((choice, idx) => { - if (idx === choiceIdx) { - return { ...choice, ...updatedAttributes }; - } - return choice; - }); - updateQuestion(questionIdx, { choices: newChoices }); + const updateChoice = (choiceIdx: number, updatedAttributes: { label: string }) => { + const newLabel = updatedAttributes.label; + const oldLabel = question.choices[choiceIdx].label; + let newChoices: any[] = []; + if (question.choices) { + newChoices = question.choices.map((choice, idx) => { + if (idx !== choiceIdx) return choice; + return { ...choice, ...updatedAttributes }; + }); + } + + let newLogic: any[] = []; + question.logic?.forEach((logic) => { + let newL: string | string[] | undefined = logic.value; + if (Array.isArray(logic.value)) { + newL = logic.value.map((value) => (value === oldLabel ? newLabel : value)); + } else { + newL = logic.value === oldLabel ? newLabel : logic.value; + } + newLogic.push({ ...logic, value: newL }); + }); + updateQuestion(questionIdx, { choices: newChoices, logic: newLogic }); }; const addChoice = (choiceIdx?: number) => { From e32e47e272db21454f29d2ffd5fe3df3985c10f0 Mon Sep 17 00:00:00 2001 From: Matti Nannt Date: Fri, 28 Jul 2023 21:08:45 +0200 Subject: [PATCH 11/19] Fix: Disable autoFocus when embedded with iframe (#615) --- .../[environmentId]/surveys/PreviewSurvey.tsx | 2 ++ apps/web/app/s/[surveyId]/LinkSurvey.tsx | 11 ++++++++++- apps/web/components/preview/OpenTextQuestion.tsx | 6 ++++-- apps/web/components/preview/QuestionConditional.tsx | 3 +++ 4 files changed, 19 insertions(+), 3 deletions(-) diff --git a/apps/web/app/(app)/environments/[environmentId]/surveys/PreviewSurvey.tsx b/apps/web/app/(app)/environments/[environmentId]/surveys/PreviewSurvey.tsx index 1410aaa64e..51a1e664d4 100644 --- a/apps/web/app/(app)/environments/[environmentId]/surveys/PreviewSurvey.tsx +++ b/apps/web/app/(app)/environments/[environmentId]/surveys/PreviewSurvey.tsx @@ -280,6 +280,7 @@ export default function PreviewSurvey({ brandColor={brandColor} lastQuestion={idx === questions.length - 1} onSubmit={gotoNextQuestion} + autoFocus={false} /> ) : null ) @@ -307,6 +308,7 @@ export default function PreviewSurvey({ brandColor={brandColor} lastQuestion={idx === questions.length - 1} onSubmit={gotoNextQuestion} + autoFocus={false} /> ) : null ) diff --git a/apps/web/app/s/[surveyId]/LinkSurvey.tsx b/apps/web/app/s/[surveyId]/LinkSurvey.tsx index d58a923a3e..6df58de670 100644 --- a/apps/web/app/s/[surveyId]/LinkSurvey.tsx +++ b/apps/web/app/s/[surveyId]/LinkSurvey.tsx @@ -11,7 +11,7 @@ import { cn } from "@formbricks/lib/cn"; import { Confetti } from "@formbricks/ui"; import { ArrowPathIcon } from "@heroicons/react/24/solid"; import type { Survey } from "@formbricks/types/surveys"; -import { useEffect, useRef } from "react"; +import { useEffect, useRef, useState } from "react"; type EnhancedSurvey = Survey & { brandColor: string; @@ -38,6 +38,14 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) { // Create a reference to the top element const topRef = useRef(null); + const [autoFocus, setAutofocus] = useState(false); + + // Not in an iframe, enable autofocus on input fields. + useEffect(() => { + if (window.self === window.top) { + setAutofocus(true); + } + }, []); // Scroll to top when the currentQuestion changes useEffect(() => { @@ -90,6 +98,7 @@ export default function LinkSurvey({ survey }: LinkSurveyProps) { brandColor={survey.brandColor} lastQuestion={lastQuestion} onSubmit={submitResponse} + autoFocus={autoFocus} /> )} diff --git a/apps/web/components/preview/OpenTextQuestion.tsx b/apps/web/components/preview/OpenTextQuestion.tsx index 296f1fb10b..b14ae918e4 100644 --- a/apps/web/components/preview/OpenTextQuestion.tsx +++ b/apps/web/components/preview/OpenTextQuestion.tsx @@ -9,6 +9,7 @@ interface OpenTextQuestionProps { onSubmit: (data: { [x: string]: any }) => void; lastQuestion: boolean; brandColor: string; + autoFocus?: boolean; } export default function OpenTextQuestion({ @@ -16,6 +17,7 @@ export default function OpenTextQuestion({ onSubmit, lastQuestion, brandColor, + autoFocus = false, }: OpenTextQuestionProps) { const [value, setValue] = useState(""); @@ -35,7 +37,7 @@ export default function OpenTextQuestion({
{question.longAnswer === false ? ( ) : (