mirror of
https://github.com/formbricks/formbricks.git
synced 2025-12-22 06:00:51 -06:00
Compare commits
6 Commits
docs/custo
...
release/4.
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
01d85f66dc | ||
|
|
4f6959ce38 | ||
|
|
5e928c2ce7 | ||
|
|
1bd43a9156 | ||
|
|
c585c83dbc | ||
|
|
2f4fa9e3cc |
6
.github/workflows/formbricks-release.yml
vendored
6
.github/workflows/formbricks-release.yml
vendored
@@ -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' }}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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 (
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
Reference in New Issue
Block a user