Compare commits

...

6 Commits

Author SHA1 Message Date
Matti Nannt
01d85f66dc fix: pin Prisma CLI to version 6 in Dockerfile (backport) (#6872) 2025-11-21 01:33:00 -08:00
Matti Nannt
4f6959ce38 fix: release pipeline boolean comparison for is_latest output (backport to 4.2) (#6871) 2025-11-21 14:39:39 +05:30
Johannes
5e928c2ce7 fix: (backport) Matrix filter (#6867) 2025-11-21 12:47:09 +05:30
Johannes
1bd43a9156 fix: (backport) Link metadata (#6866) 2025-11-21 10:44:15 +05:30
Dhruwang Jariwala
c585c83dbc fix: (backport) filters not persisting in response page (#6863) 2025-11-20 07:15:03 -08:00
Dhruwang Jariwala
2f4fa9e3cc fix: (backport) update preview when props change (#6861) 2025-11-20 05:26:34 -08:00
9 changed files with 58 additions and 44 deletions

View File

@@ -89,7 +89,7 @@ jobs:
- check-latest-release - check-latest-release
with: with:
IS_PRERELEASE: ${{ github.event.release.prerelease }} IS_PRERELEASE: ${{ github.event.release.prerelease }}
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest }} MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
docker-build-cloud: docker-build-cloud:
name: Build & push Formbricks Cloud to ECR name: Build & push Formbricks Cloud to ECR
@@ -101,7 +101,7 @@ jobs:
with: with:
image_tag: ${{ needs.docker-build-community.outputs.VERSION }} image_tag: ${{ needs.docker-build-community.outputs.VERSION }}
IS_PRERELEASE: ${{ github.event.release.prerelease }} IS_PRERELEASE: ${{ github.event.release.prerelease }}
MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest }} MAKE_LATEST: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}
needs: needs:
- check-latest-release - check-latest-release
- docker-build-community - docker-build-community
@@ -154,4 +154,4 @@ jobs:
release_tag: ${{ github.event.release.tag_name }} release_tag: ${{ github.event.release.tag_name }}
commit_sha: ${{ github.sha }} commit_sha: ${{ github.sha }}
is_prerelease: ${{ github.event.release.prerelease }} is_prerelease: ${{ github.event.release.prerelease }}
make_latest: ${{ needs.check-latest-release.outputs.is_latest }} make_latest: ${{ needs.check-latest-release.outputs.is_latest == 'true' }}

View File

@@ -124,7 +124,7 @@ RUN chmod -R 755 ./node_modules/@noble/hashes
COPY --from=installer /app/node_modules/zod ./node_modules/zod COPY --from=installer /app/node_modules/zod ./node_modules/zod
RUN chmod -R 755 ./node_modules/zod RUN chmod -R 755 ./node_modules/zod
RUN npm install -g prisma RUN npm install -g prisma@6
# Create a startup script to handle the conditional logic # Create a startup script to handle the conditional logic
COPY --from=installer /app/apps/web/scripts/docker/next-start.sh /home/nextjs/start.sh COPY --from=installer /app/apps/web/scripts/docker/next-start.sh /home/nextjs/start.sh

View File

@@ -96,14 +96,21 @@ export const ResponsePage = ({
} }
}, [searchParams, resetState]); }, [searchParams, resetState]);
// Only fetch if filters are applied (not on initial mount with no filters)
const hasFilters =
selectedFilter?.responseStatus !== "all" ||
(selectedFilter?.filter && selectedFilter.filter.length > 0) ||
(dateRange.from && dateRange.to);
useEffect(() => { useEffect(() => {
const fetchFilteredResponses = async () => { const fetchFilteredResponses = async () => {
try { try {
// skip call for initial mount // skip call for initial mount
if (page === null) { if (page === null && !hasFilters) {
setPage(1); setPage(1);
return; return;
} }
setPage(1);
setIsFetchingFirstPage(true); setIsFetchingFirstPage(true);
let responses: TResponseWithQuotas[] = []; let responses: TResponseWithQuotas[] = [];
@@ -126,15 +133,7 @@ export const ResponsePage = ({
setIsFetchingFirstPage(false); setIsFetchingFirstPage(false);
} }
}; };
fetchFilteredResponses();
// Only fetch if filters are applied (not on initial mount with no filters)
const hasFilters =
(selectedFilter && Object.keys(selectedFilter).length > 0) ||
(dateRange && (dateRange.from || dateRange.to));
if (hasFilters) {
fetchFilteredResponses();
}
}, [filters, responsesPerPage, selectedFilter, dateRange, surveyId]); }, [filters, responsesPerPage, selectedFilter, dateRange, surveyId]);
return ( return (

View File

@@ -4,7 +4,7 @@ import clsx from "clsx";
import { ChevronDown, ChevronUp, X } from "lucide-react"; import { ChevronDown, ChevronUp, X } from "lucide-react";
import { useMemo, useRef, useState } from "react"; import { useMemo, useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TI18nString, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox"; import { OptionsType } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionsComboBox";
import { getLocalizedValue } from "@/lib/i18n/utils"; import { getLocalizedValue } from "@/lib/i18n/utils";
import { useClickOutside } from "@/lib/utils/hooks/useClickOutside"; import { useClickOutside } from "@/lib/utils/hooks/useClickOutside";
@@ -26,8 +26,8 @@ import {
import { Input } from "@/modules/ui/components/input"; import { Input } from "@/modules/ui/components/input";
type QuestionFilterComboBoxProps = { type QuestionFilterComboBoxProps = {
filterOptions: string[] | undefined; filterOptions: (string | TI18nString)[] | undefined;
filterComboBoxOptions: string[] | undefined; filterComboBoxOptions: (string | TI18nString)[] | undefined;
filterValue: string | undefined; filterValue: string | undefined;
filterComboBoxValue: string | string[] | undefined; filterComboBoxValue: string | string[] | undefined;
onChangeFilterValue: (o: string) => void; onChangeFilterValue: (o: string) => void;
@@ -74,7 +74,7 @@ export const QuestionFilterComboBox = ({
if (!isMultiple) return filterComboBoxOptions; if (!isMultiple) return filterComboBoxOptions;
return filterComboBoxOptions?.filter((o) => { return filterComboBoxOptions?.filter((o) => {
const optionValue = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o; const optionValue = typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
return !filterComboBoxValue?.includes(optionValue); return !filterComboBoxValue?.includes(optionValue);
}); });
}, [isMultiple, filterComboBoxOptions, filterComboBoxValue, defaultLanguageCode]); }, [isMultiple, filterComboBoxOptions, filterComboBoxValue, defaultLanguageCode]);
@@ -91,14 +91,15 @@ export const QuestionFilterComboBox = ({
const filteredOptions = useMemo( const filteredOptions = useMemo(
() => () =>
options?.filter((o) => { options?.filter((o) => {
const optionValue = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o; const optionValue =
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
return optionValue.toLowerCase().includes(searchQuery.toLowerCase()); return optionValue.toLowerCase().includes(searchQuery.toLowerCase());
}), }),
[options, searchQuery, defaultLanguageCode] [options, searchQuery, defaultLanguageCode]
); );
const handleCommandItemSelect = (o: string) => { const handleCommandItemSelect = (o: string | TI18nString) => {
const value = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o; const value = typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
if (isMultiple) { if (isMultiple) {
const newValue = Array.isArray(filterComboBoxValue) ? [...filterComboBoxValue, value] : [value]; const newValue = Array.isArray(filterComboBoxValue) ? [...filterComboBoxValue, value] : [value];
@@ -200,14 +201,18 @@ export const QuestionFilterComboBox = ({
)} )}
</DropdownMenuTrigger> </DropdownMenuTrigger>
<DropdownMenuContent className="bg-white"> <DropdownMenuContent className="bg-white">
{filterOptions?.map((o, index) => ( {filterOptions?.map((o, index) => {
<DropdownMenuItem const optionValue =
key={`${o}-${index}`} typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
className="cursor-pointer" return (
onClick={() => onChangeFilterValue(o)}> <DropdownMenuItem
{o} key={`${optionValue}-${index}`}
</DropdownMenuItem> className="cursor-pointer"
))} onClick={() => onChangeFilterValue(optionValue)}>
{optionValue}
</DropdownMenuItem>
);
})}
</DropdownMenuContent> </DropdownMenuContent>
</DropdownMenu> </DropdownMenu>
)} )}
@@ -269,7 +274,8 @@ export const QuestionFilterComboBox = ({
<CommandEmpty>{t("common.no_result_found")}</CommandEmpty> <CommandEmpty>{t("common.no_result_found")}</CommandEmpty>
<CommandGroup> <CommandGroup>
{filteredOptions?.map((o) => { {filteredOptions?.map((o) => {
const optionValue = typeof o === "object" ? getLocalizedValue(o, defaultLanguageCode) : o; const optionValue =
typeof o === "object" && o !== null ? getLocalizedValue(o, defaultLanguageCode) : o;
return ( return (
<CommandItem <CommandItem
key={optionValue} key={optionValue}

View File

@@ -4,7 +4,7 @@ import { useAutoAnimate } from "@formkit/auto-animate/react";
import { ChevronDown, ChevronUp, Plus, TrashIcon } from "lucide-react"; import { ChevronDown, ChevronUp, Plus, TrashIcon } from "lucide-react";
import React, { useEffect, useState } from "react"; import React, { useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types"; import { TI18nString, TSurvey, TSurveyQuestionTypeEnum } from "@formbricks/types/surveys/types";
import { import {
SelectedFilterValue, SelectedFilterValue,
TResponseStatus, TResponseStatus,
@@ -13,6 +13,7 @@ import {
import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions"; import { getSurveyFilterDataAction } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/actions";
import { QuestionFilterComboBox } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox"; import { QuestionFilterComboBox } from "@/app/(app)/environments/[environmentId]/surveys/[surveyId]/components/QuestionFilterComboBox";
import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys"; import { generateQuestionAndFilterOptions } from "@/app/lib/surveys/surveys";
import { getLocalizedValue } from "@/lib/i18n/utils";
import { Button } from "@/modules/ui/components/button"; import { Button } from "@/modules/ui/components/button";
import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/components/popover"; import { Popover, PopoverContent, PopoverTrigger } from "@/modules/ui/components/popover";
import { import {
@@ -26,8 +27,8 @@ import { OptionsType, QuestionOption, QuestionsComboBox } from "./QuestionsCombo
export type QuestionFilterOptions = { export type QuestionFilterOptions = {
type: TSurveyQuestionTypeEnum | "Attributes" | "Tags" | "Languages" | "Quotas"; type: TSurveyQuestionTypeEnum | "Attributes" | "Tags" | "Languages" | "Quotas";
filterOptions: string[]; filterOptions: (string | TI18nString)[];
filterComboBoxOptions: string[]; filterComboBoxOptions: (string | TI18nString)[];
id: string; id: string;
}; };
@@ -69,6 +70,12 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
const [isOpen, setIsOpen] = useState<boolean>(false); const [isOpen, setIsOpen] = useState<boolean>(false);
const [filterValue, setFilterValue] = useState<SelectedFilterValue>(selectedFilter); const [filterValue, setFilterValue] = useState<SelectedFilterValue>(selectedFilter);
const getDefaultFilterValue = (option?: QuestionFilterOptions): string | undefined => {
if (!option || option.filterOptions.length === 0) return undefined;
const firstOption = option.filterOptions[0];
return typeof firstOption === "object" ? getLocalizedValue(firstOption, "default") : firstOption;
};
useEffect(() => { useEffect(() => {
// Fetch the initial data for the filter and load it into the state // Fetch the initial data for the filter and load it into the state
const handleInitialData = async () => { const handleInitialData = async () => {
@@ -94,15 +101,18 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
}, [isOpen, setSelectedOptions, survey]); }, [isOpen, setSelectedOptions, survey]);
const handleOnChangeQuestionComboBoxValue = (value: QuestionOption, index: number) => { const handleOnChangeQuestionComboBoxValue = (value: QuestionOption, index: number) => {
const matchingFilterOption = selectedOptions.questionFilterOptions.find(
(q) => q.type === value.type || q.type === value.questionType
);
const defaultFilterValue = getDefaultFilterValue(matchingFilterOption);
if (filterValue.filter[index].questionType) { if (filterValue.filter[index].questionType) {
// Create a new array and copy existing values from SelectedFilter // Create a new array and copy existing values from SelectedFilter
filterValue.filter[index] = { filterValue.filter[index] = {
questionType: value, questionType: value,
filterType: { filterType: {
filterComboBoxValue: undefined, filterComboBoxValue: undefined,
filterValue: selectedOptions.questionFilterOptions.find( filterValue: defaultFilterValue,
(q) => q.type === value.type || q.type === value.questionType
)?.filterOptions[0],
}, },
}; };
setFilterValue({ filter: [...filterValue.filter], responseStatus: filterValue.responseStatus }); setFilterValue({ filter: [...filterValue.filter], responseStatus: filterValue.responseStatus });
@@ -111,9 +121,7 @@ export const ResponseFilter = ({ survey }: ResponseFilterProps) => {
filterValue.filter[index].questionType = value; filterValue.filter[index].questionType = value;
filterValue.filter[index].filterType = { filterValue.filter[index].filterType = {
filterComboBoxValue: undefined, filterComboBoxValue: undefined,
filterValue: selectedOptions.questionFilterOptions.find( filterValue: defaultFilterValue,
(q) => q.type === value.type || q.type === value.questionType
)?.filterOptions[0],
}; };
setFilterValue({ ...filterValue }); setFilterValue({ ...filterValue });
} }

View File

@@ -213,8 +213,8 @@ describe("surveys", () => {
id: "q8", id: "q8",
type: TSurveyQuestionTypeEnum.Matrix, type: TSurveyQuestionTypeEnum.Matrix,
headline: { default: "Matrix" }, headline: { default: "Matrix" },
rows: [{ id: "r1", label: "Row 1" }], rows: [{ id: "r1", label: { default: "Row 1" } }],
columns: [{ id: "c1", label: "Column 1" }], columns: [{ id: "c1", label: { default: "Column 1" } }],
} as unknown as TSurveyQuestion, } as unknown as TSurveyQuestion,
], ],
createdAt: new Date(), createdAt: new Date(),

View File

@@ -121,8 +121,8 @@ export const generateQuestionAndFilterOptions = (
} else if (q.type === TSurveyQuestionTypeEnum.Matrix) { } else if (q.type === TSurveyQuestionTypeEnum.Matrix) {
questionFilterOptions.push({ questionFilterOptions.push({
type: q.type, type: q.type,
filterOptions: q.rows.flatMap((row) => Object.values(row)), filterOptions: q.rows.map((row) => getLocalizedValue(row.label, "default")),
filterComboBoxOptions: q.columns.flatMap((column) => Object.values(column)), filterComboBoxOptions: q.columns.map((column) => getLocalizedValue(column.label, "default")),
id: q.id, id: q.id,
}); });
} else { } else {

View File

@@ -57,6 +57,7 @@ export const getSurveyWithMetadata = reactCache(async (surveyId: string) => {
surveyClosedMessage: true, surveyClosedMessage: true,
showLanguageSwitch: true, showLanguageSwitch: true,
recaptcha: true, recaptcha: true,
metadata: true,
// Related data // Related data
languages: { languages: {

View File

@@ -80,7 +80,7 @@ export const SurveyInline = (props: Omit<SurveyContainerProps, "containerId">) =
loadScript(); loadScript();
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, [props]);
useEffect(() => { useEffect(() => {
if (isScriptLoaded) { if (isScriptLoaded) {